implement update function for plugins on frontend / adjust the plugin pending modal to show pending installations and updates

This commit is contained in:
Eduard Heimbuch
2019-09-16 14:31:55 +02:00
parent ba59713c7f
commit d60918c820
14 changed files with 568 additions and 84 deletions

View File

@@ -4,6 +4,7 @@ import type {Collection, Links} from "./hal";
export type Plugin = { export type Plugin = {
name: string, name: string,
version: string, version: string,
newVersion: string,
displayName: string, displayName: string,
description?: string, description?: string,
author: string, author: string,
@@ -24,3 +25,11 @@ export type PluginGroup = {
name: string, name: string,
plugins: Plugin[] plugins: Plugin[]
}; };
export type PendingPlugins = {
_links: Links,
_embedded: {
new: [],
update: [],
}
}

View File

@@ -25,7 +25,7 @@ export type { SubRepository, File } from "./Sources";
export type { SelectValue, AutocompleteObject } from "./Autocomplete"; export type { SelectValue, AutocompleteObject } from "./Autocomplete";
export type { Plugin, PluginCollection, PluginGroup } from "./Plugin"; export type { Plugin, PluginCollection, PluginGroup, PendingPlugins } from "./Plugin";
export type { RepositoryRole } from "./RepositoryRole"; export type { RepositoryRole } from "./RepositoryRole";

View File

@@ -29,22 +29,32 @@
"installedNavLink": "Installiert", "installedNavLink": "Installiert",
"availableNavLink": "Verfügbar" "availableNavLink": "Verfügbar"
}, },
"installPending": "Austehende Plugins installieren", "executePending": "Austehende Plugin-Änderungen ausführen",
"noPlugins": "Keine Plugins gefunden.", "noPlugins": "Keine Plugins gefunden.",
"modal": { "modal": {
"title": "{{name}} Plugin installieren", "title": {
"install": "{{name}} Plugin installieren",
"update": "{{name}} Plugin aktualisieren"
},
"restart": "Neustarten um Plugin zu aktivieren", "restart": "Neustarten um Plugin zu aktivieren",
"install": "Installieren", "install": "Installieren",
"update": "Aktualisieren",
"installQueue": "Werden installiert:",
"updateQueue": "Werden aktualisiert:",
"installAndRestart": "Installieren und Neustarten", "installAndRestart": "Installieren und Neustarten",
"updateAndRestart": "Aktualisieren und Neustarten",
"executeAndRestart": "Ausführen und Neustarten",
"abort": "Abbrechen", "abort": "Abbrechen",
"author": "Autor", "author": "Autor",
"version": "Version", "version": "Version",
"currentVersion": "Installierte Version",
"newVersion": "Neue Version",
"dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installiert, wenn sie noch nicht vorhanden sind!", "dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installiert, wenn sie noch nicht vorhanden sind!",
"dependencies": "Abhängigkeiten", "dependencies": "Abhängigkeiten",
"successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:", "successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
"reload": "jetzt neu laden", "reload": "jetzt neu laden",
"restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.", "restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.",
"installPending": "Die folgenden Plugins werden installiert. Anschließend wird der SCM-Manager Kontext neu gestartet." "executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet."
} }
}, },
"repositoryRole": { "repositoryRole": {

View File

@@ -29,22 +29,32 @@
"installedNavLink": "Installed", "installedNavLink": "Installed",
"availableNavLink": "Available" "availableNavLink": "Available"
}, },
"installPending": "Install pending plugins", "executePending": "Execute pending plugin changes",
"noPlugins": "No plugins found.", "noPlugins": "No plugins found.",
"modal": { "modal": {
"title": "Install {{name}} Plugin", "title": {
"install": "Install {{name}} Plugin",
"update": "Update {{name}} Plugin"
},
"restart": "Restart to activate", "restart": "Restart to activate",
"install": "Install", "install": "Install",
"update": "Update",
"installQueue": "Will be installed:",
"updateQueue": "Will be updated:",
"installAndRestart": "Install and Restart", "installAndRestart": "Install and Restart",
"updateAndRestart": "Update and Restart",
"executeAndRestart": "Execute and Restart",
"abort": "Abort", "abort": "Abort",
"author": "Author", "author": "Author",
"version": "Version", "version": "Version",
"currentVersion": "Installed version",
"newVersion": "New version",
"dependencyNotification": "With this plugin, the following dependencies will be installed if they are not available yet!", "dependencyNotification": "With this plugin, the following dependencies will be installed if they are not available yet!",
"dependencies": "Dependencies", "dependencies": "Dependencies",
"successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:", "successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:",
"reload": "reload now", "reload": "reload now",
"restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.", "restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.",
"installPending": "The following plugins will be installed and after installation the scm-manager context will be restarted." "executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted."
} }
}, },
"repositoryRole": { "repositoryRole": {

View File

@@ -1,12 +1,12 @@
// @flow // @flow
import React from "react"; import React from "react";
import { Button } from "@scm-manager/ui-components"; import { Button } from "@scm-manager/ui-components";
import type { PluginCollection } from "@scm-manager/ui-types"; import type { PendingPlugins } from "@scm-manager/ui-types";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import InstallPendingModal from "./InstallPendingModal"; import ExecutePendingModal from "./ExecutePendingModal";
type Props = { type Props = {
collection: PluginCollection, pendingPlugins: PendingPlugins,
// context props // context props
t: string => string t: string => string
@@ -16,7 +16,7 @@ type State = {
showModal: boolean showModal: boolean
}; };
class InstallPendingAction extends React.Component<Props, State> { class ExecutePendingAction extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@@ -38,11 +38,11 @@ class InstallPendingAction extends React.Component<Props, State> {
renderModal = () => { renderModal = () => {
const { showModal } = this.state; const { showModal } = this.state;
const { collection } = this.props; const { pendingPlugins } = this.props;
if (showModal) { if (showModal) {
return ( return (
<InstallPendingModal <ExecutePendingModal
collection={collection} pendingPlugins={pendingPlugins}
onClose={this.closeModal} onClose={this.closeModal}
/> />
); );
@@ -57,7 +57,7 @@ class InstallPendingAction extends React.Component<Props, State> {
{this.renderModal()} {this.renderModal()}
<Button <Button
color="primary" color="primary"
label={t("plugins.installPending")} label={t("plugins.executePending")}
action={this.openModal} action={this.openModal}
/> />
</> </>
@@ -65,4 +65,4 @@ class InstallPendingAction extends React.Component<Props, State> {
} }
} }
export default translate("admin")(InstallPendingAction); export default translate("admin")(ExecutePendingAction);

View File

@@ -8,14 +8,14 @@ import {
Modal, Modal,
Notification Notification
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import type { PluginCollection } from "@scm-manager/ui-types"; import type { PendingPlugins } from "@scm-manager/ui-types";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import waitForRestart from "./waitForRestart"; import waitForRestart from "./waitForRestart";
import InstallSuccessNotification from "./InstallSuccessNotification"; import InstallSuccessNotification from "./SuccessNotification";
type Props = { type Props = {
onClose: () => void, onClose: () => void,
collection: PluginCollection, pendingPlugins: PendingPlugins,
// context props // context props
t: string => string t: string => string
@@ -27,7 +27,7 @@ type State = {
error?: Error error?: Error
}; };
class InstallPendingModal extends React.Component<Props, State> { class ExecutePendingModal extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@@ -53,13 +53,13 @@ class InstallPendingModal extends React.Component<Props, State> {
}; };
installAndRestart = () => { installAndRestart = () => {
const { collection } = this.props; const { pendingPlugins } = this.props;
this.setState({ this.setState({
loading: true loading: true
}); });
apiClient apiClient
.post(collection._links.installPending.href) .post(pendingPlugins._links.execute.href)
.then(waitForRestart) .then(waitForRestart)
.then(() => { .then(() => {
this.setState({ this.setState({
@@ -77,22 +77,57 @@ class InstallPendingModal extends React.Component<Props, State> {
}); });
}; };
renderInstallQueue = () => {
const { pendingPlugins, t } = this.props;
return (
<>
{pendingPlugins._embedded &&
pendingPlugins._embedded.new.length > 0 && (
<>
<strong>{t("plugins.modal.installQueue")}</strong>
<ul>
{pendingPlugins._embedded.new
.filter(plugin => plugin.pending)
.map(plugin => (
<li key={plugin.name}>{plugin.name}</li>
))}
</ul>
</>
)}
</>
);
};
renderUpdateQueue = () => {
const { pendingPlugins, t } = this.props;
return (
<>
{pendingPlugins._embedded &&
pendingPlugins._embedded.update.length > 0 && (
<>
<strong>{t("plugins.modal.updateQueue")}</strong>
<ul>
{pendingPlugins._embedded.update
.filter(plugin => plugin.pending)
.map(plugin => (
<li key={plugin.name}>{plugin.name}</li>
))}
</ul>
</>
)}
</>
);
};
renderBody = () => { renderBody = () => {
const { collection, t } = this.props; const { t } = this.props;
return ( return (
<> <>
<div className="media"> <div className="media">
<div className="content"> <div className="content">
<p>{t("plugins.modal.installPending")}</p> <p>{t("plugins.modal.executePending")}</p>
<ul> {this.renderInstallQueue()}
{collection._embedded.plugins {this.renderUpdateQueue()}
.filter(plugin => plugin.pending)
.map(plugin => (
<li key={plugin.name} className="has-text-weight-bold">
{plugin.name}
</li>
))}
</ul>
</div> </div>
</div> </div>
<div className="media">{this.renderNotifications()}</div> <div className="media">{this.renderNotifications()}</div>
@@ -107,7 +142,7 @@ class InstallPendingModal extends React.Component<Props, State> {
<ButtonGroup> <ButtonGroup>
<Button <Button
color="warning" color="warning"
label={t("plugins.modal.installAndRestart")} label={t("plugins.modal.executeAndRestart")}
loading={loading} loading={loading}
action={this.installAndRestart} action={this.installAndRestart}
disabled={error || success} disabled={error || success}
@@ -121,7 +156,7 @@ class InstallPendingModal extends React.Component<Props, State> {
const { onClose, t } = this.props; const { onClose, t } = this.props;
return ( return (
<Modal <Modal
title={t("plugins.modal.installAndRestart")} title={t("plugins.modal.executeAndRestart")}
closeFunction={onClose} closeFunction={onClose}
body={this.renderBody()} body={this.renderBody()}
footer={this.renderFooter()} footer={this.renderFooter()}
@@ -131,4 +166,4 @@ class InstallPendingModal extends React.Component<Props, State> {
} }
} }
export default translate("admin")(InstallPendingModal); export default translate("admin")(ExecutePendingModal);

View File

@@ -15,7 +15,7 @@ import {
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import classNames from "classnames"; import classNames from "classnames";
import waitForRestart from "./waitForRestart"; import waitForRestart from "./waitForRestart";
import InstallSuccessNotification from "./InstallSuccessNotification"; import SuccessNotification from "./SuccessNotification";
type Props = { type Props = {
plugin: Plugin, plugin: Plugin,
@@ -45,7 +45,7 @@ const styles = {
} }
}; };
class PluginModal extends React.Component<Props, State> { class InstallPluginModal extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@@ -162,7 +162,7 @@ class PluginModal extends React.Component<Props, State> {
} else if (success) { } else if (success) {
return ( return (
<div className="media"> <div className="media">
<InstallSuccessNotification /> <SuccessNotification />
</div> </div>
); );
} else if (restart) { } else if (restart) {
@@ -252,7 +252,7 @@ class PluginModal extends React.Component<Props, State> {
return ( return (
<Modal <Modal
title={t("plugins.modal.title", { title={t("plugins.modal.title.install", {
name: plugin.displayName ? plugin.displayName : plugin.name name: plugin.displayName ? plugin.displayName : plugin.name
})} })}
closeFunction={() => onClose()} closeFunction={() => onClose()}
@@ -267,4 +267,4 @@ class PluginModal extends React.Component<Props, State> {
export default compose( export default compose(
injectSheet(styles), injectSheet(styles),
translate("admin") translate("admin")
)(PluginModal); )(InstallPluginModal);

View File

@@ -4,8 +4,9 @@ import injectSheet from "react-jss";
import type { Plugin } from "@scm-manager/ui-types"; import type { Plugin } from "@scm-manager/ui-types";
import { CardColumn } from "@scm-manager/ui-components"; import { CardColumn } from "@scm-manager/ui-components";
import PluginAvatar from "./PluginAvatar"; import PluginAvatar from "./PluginAvatar";
import PluginModal from "./PluginModal";
import classNames from "classnames"; import classNames from "classnames";
import InstallPluginModal from "./InstallPluginModal";
import UpdatePluginModal from "./UpdatePluginModal";
type Props = { type Props = {
plugin: Plugin, plugin: Plugin,
@@ -22,9 +23,15 @@ type State = {
const styles = { const styles = {
link: { link: {
cursor: "pointer", cursor: "pointer",
pointerEvents: "all" pointerEvents: "all",
padding: "0.5rem",
border: "solid 1px var(--dark-25)",
borderRadius: "4px",
"&:hover": {
borderColor: "var(--dark-50)"
}
}, },
spinner: { topRight: {
position: "absolute", position: "absolute",
right: 0, right: 0,
top: 0 top: 0
@@ -59,17 +66,52 @@ class PluginEntry extends React.Component<Props, State> {
return plugin._links && plugin._links.install && plugin._links.install.href; return plugin._links && plugin._links.install && plugin._links.install.href;
}; };
createFooterLeft = () => { isUpdatable = () => {
const { plugin } = this.props;
return plugin._links && plugin._links.update && plugin._links.update.href;
};
createActionbar = () => {
const { classes } = this.props; const { classes } = this.props;
if (this.isInstallable()) { if (this.isInstallable()) {
return ( return (
<span <span
className={classNames(classes.link, "level-item")} className={classNames(classes.link, classes.topRight, "level-item")}
onClick={this.toggleModal} onClick={this.toggleModal}
> >
<i className="fas fa-download has-text-info" /> <i className="fas fa-download has-text-info" />
</span> </span>
); );
} else if (this.isUpdatable()) {
return (
<span
className={classNames(classes.link, classes.topRight, "level-item")}
onClick={this.toggleModal}
>
<i className="fas fa-sync-alt has-text-info" />
</span>
);
}
};
renderModal = () => {
const { plugin, refresh } = this.props;
if (this.isInstallable()) {
return (
<InstallPluginModal
plugin={plugin}
refresh={refresh}
onClose={this.toggleModal}
/>
);
} else if (this.isUpdatable()) {
return (
<UpdatePluginModal
plugin={plugin}
refresh={refresh}
onClose={this.toggleModal}
/>
);
} }
}; };
@@ -77,7 +119,7 @@ class PluginEntry extends React.Component<Props, State> {
const { plugin, classes } = this.props; const { plugin, classes } = this.props;
if (plugin.pending) { if (plugin.pending) {
return ( return (
<span className={classes.spinner}> <span className={classes.topRight}>
<i className="fas fa-spinner fa-spin has-text-info" /> <i className="fas fa-spinner fa-spin has-text-info" />
</span> </span>
); );
@@ -86,19 +128,13 @@ class PluginEntry extends React.Component<Props, State> {
}; };
render() { render() {
const { plugin, refresh } = this.props; const { plugin } = this.props;
const { showModal } = this.state; const { showModal } = this.state;
const avatar = this.createAvatar(plugin); const avatar = this.createAvatar(plugin);
const footerLeft = this.createFooterLeft(); const actionbar = this.createActionbar();
const footerRight = this.createFooterRight(plugin); const footerRight = this.createFooterRight(plugin);
const modal = showModal ? ( const modal = showModal ? this.renderModal() : null;
<PluginModal
plugin={plugin}
refresh={refresh}
onClose={this.toggleModal}
/>
) : null;
return ( return (
<> <>
@@ -107,8 +143,9 @@ class PluginEntry extends React.Component<Props, State> {
avatar={avatar} avatar={avatar}
title={plugin.displayName ? plugin.displayName : plugin.name} title={plugin.displayName ? plugin.displayName : plugin.name}
description={plugin.description} description={plugin.description}
contentRight={this.createPendingSpinner()} contentRight={
footerLeft={footerLeft} plugin.pending ? this.createPendingSpinner() : actionbar
}
footerRight={footerRight} footerRight={footerRight}
/> />
{modal} {modal}

View File

@@ -0,0 +1,288 @@
//@flow
import React from "react";
import { compose } from "redux";
import { translate } from "react-i18next";
import injectSheet from "react-jss";
import type { Plugin } from "@scm-manager/ui-types";
import {
apiClient,
Button,
ButtonGroup,
Checkbox,
ErrorNotification,
Modal,
Notification
} from "@scm-manager/ui-components";
import classNames from "classnames";
import waitForRestart from "./waitForRestart";
import SuccessNotification from "./SuccessNotification";
type Props = {
plugin: Plugin,
refresh: () => void,
onClose: () => void,
// context props
classes: any,
t: (key: string, params?: Object) => string
};
type State = {
success: boolean,
restart: boolean,
loading: boolean,
error?: Error
};
const styles = {
userLabelAlignment: {
textAlign: "left",
marginRight: 0,
minWidth: "9em"
},
userFieldFlex: {
flexGrow: 4
}
};
class UpdatePluginModal extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
loading: false,
restart: false,
success: false
};
}
onUpdateSuccess = () => {
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();
});
}
};
update = (e: Event) => {
const { restart } = this.state;
const { plugin } = this.props;
this.setState({
loading: true
});
e.preventDefault();
apiClient
.post(plugin._links.update.href + "?restart=" + restart.toString())
.then(this.onUpdateSuccess)
.catch(error => {
this.setState({
loading: false,
error: error
});
});
};
footer = () => {
const { onClose, t } = this.props;
const { loading, error, restart, success } = this.state;
let color = "primary";
let label = "plugins.modal.update";
if (restart) {
color = "warning";
label = "plugins.modal.updateAndRestart";
}
return (
<ButtonGroup>
<Button
label={t(label)}
color={color}
action={this.update}
loading={loading}
disabled={!!error || success}
/>
<Button label={t("plugins.modal.abort")} action={onClose} />
</ButtonGroup>
);
};
renderDependencies() {
const { plugin, classes, t } = this.props;
let dependencies = null;
if (plugin.dependencies && plugin.dependencies.length > 0) {
dependencies = (
<div className="media">
<Notification type="warning">
<strong>{t("plugins.modal.dependencyNotification")}</strong>
<ul className={classes.listSpacing}>
{plugin.dependencies.map((dependency, index) => {
return <li key={index}>{dependency}</li>;
})}
</ul>
</Notification>
</div>
);
}
return dependencies;
}
renderNotifications = () => {
const { t } = this.props;
const { restart, error, success } = this.state;
if (error) {
return (
<div className="media">
<ErrorNotification error={error} />
</div>
);
} else if (success) {
return (
<div className="media">
<SuccessNotification />
</div>
);
} else if (restart) {
return (
<div className="media">
<Notification type="warning">
{t("plugins.modal.restartNotification")}
</Notification>
</div>
);
}
return null;
};
handleRestartChange = (value: boolean) => {
this.setState({
restart: value
});
};
render() {
const { restart } = this.state;
const { plugin, onClose, classes, t } = this.props;
const body = (
<>
<div className="media">
<div className="media-content">
<p>{plugin.description}</p>
</div>
</div>
<div className="media">
<div className="media-content">
<div className="field is-horizontal">
<div
className={classNames(
classes.userLabelAlignment,
"field-label is-inline-flex"
)}
>
{t("plugins.modal.author")}:
</div>
<div
className={classNames(
classes.userFieldFlex,
"field-body is-inline-flex"
)}
>
{plugin.author}
</div>
</div>
<div className="field is-horizontal">
<div
className={classNames(
classes.userLabelAlignment,
"field-label is-inline-flex"
)}
>
{t("plugins.modal.currentVersion")}:
</div>
<div
className={classNames(
classes.userFieldFlex,
"field-body is-inline-flex"
)}
>
{plugin.version}
</div>
</div>
<div className="field is-horizontal">
<div
className={classNames(
classes.userLabelAlignment,
"field-label is-inline-flex"
)}
>
{t("plugins.modal.newVersion")}:
</div>
<div
className={classNames(
classes.userFieldFlex,
"field-body is-inline-flex"
)}
>
{plugin.newVersion}
</div>
</div>
{this.renderDependencies()}
</div>
</div>
<div className="media">
<div className="media-content">
<Checkbox
checked={restart}
label={t("plugins.modal.restart")}
onChange={this.handleRestartChange}
disabled={false}
/>
</div>
</div>
{this.renderNotifications()}
</>
);
return (
<Modal
title={t("plugins.modal.title.update", {
name: plugin.displayName ? plugin.displayName : plugin.name
})}
closeFunction={() => onClose()}
body={body}
footer={this.footer()}
active={true}
/>
);
}
}
export default compose(
injectSheet(styles),
translate("admin")
)(UpdatePluginModal);

View File

@@ -3,28 +3,31 @@ import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { compose } from "redux"; import { compose } from "redux";
import type { PluginCollection } from "@scm-manager/ui-types"; import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
import { import {
ErrorNotification,
Loading, Loading,
Title,
Subtitle,
Notification, Notification,
ErrorNotification Subtitle,
Title
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import { import {
fetchPendingPlugins,
fetchPluginsByLink, fetchPluginsByLink,
getFetchPluginsFailure, getFetchPluginsFailure,
getPendingPlugins,
getPluginCollection, getPluginCollection,
isFetchPluginsPending isFetchPluginsPending
} from "../modules/plugins"; } from "../modules/plugins";
import PluginsList from "../components/PluginList"; import PluginsList from "../components/PluginList";
import { import {
getAvailablePluginsLink, getAvailablePluginsLink,
getInstalledPluginsLink getInstalledPluginsLink,
getPendingPluginsLink
} from "../../../modules/indexResource"; } from "../../../modules/indexResource";
import PluginTopActions from "../components/PluginTopActions"; import PluginTopActions from "../components/PluginTopActions";
import PluginBottomActions from "../components/PluginBottomActions"; import PluginBottomActions from "../components/PluginBottomActions";
import InstallPendingAction from "../components/InstallPendingAction"; import ExecutePendingAction from "../components/ExecutePendingAction";
type Props = { type Props = {
loading: boolean, loading: boolean,
@@ -34,12 +37,15 @@ type Props = {
installed: boolean, installed: boolean,
availablePluginsLink: string, availablePluginsLink: string,
installedPluginsLink: string, installedPluginsLink: string,
pendingPluginsLink: string,
pendingPlugins: PendingPlugins,
// context objects // context objects
t: string => string, t: string => string,
// dispatched functions // dispatched functions
fetchPluginsByLink: (link: string) => void fetchPluginsByLink: (link: string) => void,
fetchPendingPlugins: (link: string) => void
}; };
class PluginsOverview extends React.Component<Props> { class PluginsOverview extends React.Component<Props> {
@@ -48,15 +54,16 @@ class PluginsOverview extends React.Component<Props> {
installed, installed,
fetchPluginsByLink, fetchPluginsByLink,
availablePluginsLink, availablePluginsLink,
installedPluginsLink installedPluginsLink,
pendingPluginsLink,
fetchPendingPlugins
} = this.props; } = this.props;
fetchPluginsByLink(installed ? installedPluginsLink : availablePluginsLink); fetchPluginsByLink(installed ? installedPluginsLink : availablePluginsLink);
fetchPendingPlugins(pendingPluginsLink);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const { installed } = this.props;
installed,
} = this.props;
if (prevProps.installed !== installed) { if (prevProps.installed !== installed) {
this.fetchPlugins(); this.fetchPlugins();
} }
@@ -67,11 +74,12 @@ class PluginsOverview extends React.Component<Props> {
installed, installed,
fetchPluginsByLink, fetchPluginsByLink,
availablePluginsLink, availablePluginsLink,
installedPluginsLink installedPluginsLink,
pendingPluginsLink,
fetchPendingPlugins
} = this.props; } = this.props;
fetchPluginsByLink( fetchPluginsByLink(installed ? installedPluginsLink : availablePluginsLink);
installed ? installedPluginsLink : availablePluginsLink fetchPendingPlugins(pendingPluginsLink);
);
}; };
renderHeader = (actions: React.Node) => { renderHeader = (actions: React.Node) => {
@@ -101,9 +109,13 @@ class PluginsOverview extends React.Component<Props> {
}; };
createActions = () => { createActions = () => {
const { collection } = this.props; const { pendingPlugins } = this.props;
if (collection._links.installPending) { if (
return <InstallPendingAction collection={collection} />; pendingPlugins &&
pendingPlugins._links &&
pendingPlugins._links.execute
) {
return <ExecutePendingAction pendingPlugins={pendingPlugins} />;
} }
return null; return null;
}; };
@@ -134,7 +146,12 @@ class PluginsOverview extends React.Component<Props> {
const { collection, t } = this.props; const { collection, t } = this.props;
if (collection._embedded && collection._embedded.plugins.length > 0) { if (collection._embedded && collection._embedded.plugins.length > 0) {
return <PluginsList plugins={collection._embedded.plugins} refresh={this.fetchPlugins} />; return (
<PluginsList
plugins={collection._embedded.plugins}
refresh={this.fetchPlugins}
/>
);
} }
return <Notification type="info">{t("plugins.noPlugins")}</Notification>; return <Notification type="info">{t("plugins.noPlugins")}</Notification>;
} }
@@ -146,13 +163,17 @@ const mapStateToProps = state => {
const error = getFetchPluginsFailure(state); const error = getFetchPluginsFailure(state);
const availablePluginsLink = getAvailablePluginsLink(state); const availablePluginsLink = getAvailablePluginsLink(state);
const installedPluginsLink = getInstalledPluginsLink(state); const installedPluginsLink = getInstalledPluginsLink(state);
const pendingPluginsLink = getPendingPluginsLink(state);
const pendingPlugins = getPendingPlugins(state);
return { return {
collection, collection,
loading, loading,
error, error,
availablePluginsLink, availablePluginsLink,
installedPluginsLink installedPluginsLink,
pendingPluginsLink,
pendingPlugins
}; };
}; };
@@ -160,6 +181,9 @@ const mapDispatchToProps = dispatch => {
return { return {
fetchPluginsByLink: (link: string) => { fetchPluginsByLink: (link: string) => {
dispatch(fetchPluginsByLink(link)); dispatch(fetchPluginsByLink(link));
},
fetchPendingPlugins: (link: string) => {
dispatch(fetchPendingPlugins(link));
} }
}; };
}; };

View File

@@ -15,6 +15,17 @@ export const FETCH_PLUGIN_PENDING = `${FETCH_PLUGIN}_${types.PENDING_SUFFIX}`;
export const FETCH_PLUGIN_SUCCESS = `${FETCH_PLUGIN}_${types.SUCCESS_SUFFIX}`; export const FETCH_PLUGIN_SUCCESS = `${FETCH_PLUGIN}_${types.SUCCESS_SUFFIX}`;
export const FETCH_PLUGIN_FAILURE = `${FETCH_PLUGIN}_${types.FAILURE_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 // fetch plugins
export function fetchPluginsByLink(link: string) { export function fetchPluginsByLink(link: string) {
return function(dispatch: any) { return function(dispatch: any) {
@@ -105,8 +116,44 @@ export function fetchPluginFailure(name: string, error: Error): Action {
}; };
} }
// 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 // reducer
function normalizeByName(pluginCollection: PluginCollection) { function normalizeByName(state: Object, pluginCollection: PluginCollection) {
const names = []; const names = [];
const byNames = {}; const byNames = {};
for (const plugin of pluginCollection._embedded.plugins) { for (const plugin of pluginCollection._embedded.plugins) {
@@ -114,6 +161,7 @@ function normalizeByName(pluginCollection: PluginCollection) {
byNames[plugin.name] = plugin; byNames[plugin.name] = plugin;
} }
return { return {
...state,
list: { list: {
...pluginCollection, ...pluginCollection,
_embedded: { _embedded: {
@@ -144,9 +192,11 @@ export default function reducer(
switch (action.type) { switch (action.type) {
case FETCH_PLUGINS_SUCCESS: case FETCH_PLUGINS_SUCCESS:
return normalizeByName(action.payload); return normalizeByName(state, action.payload);
case FETCH_PLUGIN_SUCCESS: case FETCH_PLUGIN_SUCCESS:
return reducerByNames(state, action.payload); return reducerByNames(state, action.payload);
case FETCH_PENDING_PLUGINS_SUCCESS:
return { ...state, pending: action.payload };
default: default:
return state; return state;
} }
@@ -189,3 +239,17 @@ export function isFetchPluginPending(state: Object, name: string) {
export function getFetchPluginFailure(state: Object, name: string) { export function getFetchPluginFailure(state: Object, name: string) {
return getFailure(state, FETCH_PLUGIN, name); 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);
}

View File

@@ -124,6 +124,10 @@ export function getInstalledPluginsLink(state: Object) {
return getLink(state, "installedPlugins"); return getLink(state, "installedPlugins");
} }
export function getPendingPluginsLink(state: Object) {
return getLink(state, "pendingPlugins");
}
export function getMeLink(state: Object) { export function getMeLink(state: Object) {
return getLink(state, "me"); return getLink(state, "me");
} }

View File

@@ -64,13 +64,16 @@ public class PendingPluginResource {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self()); Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self());
if (!pending.isEmpty()) { List<PluginDto> newPluginDtos = newPlugins.map(mapper::mapAvailable).collect(toList());
linksBuilder.single(link("install", resourceLinks.pendingPluginCollection().installPending())); List<PluginDto> updatePluginDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
if (newPluginDtos.size() > 0 || updatePluginDtos.size() > 0) {
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().installPending()));
} }
Embedded.Builder embedded = Embedded.embeddedBuilder(); Embedded.Builder embedded = Embedded.embeddedBuilder();
embedded.with("new", newPlugins.map(mapper::mapAvailable).collect(toList())); embedded.with("new", newPluginDtos);
embedded.with("update", updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList())); embedded.with("update", updatePluginDtos);
return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build(); return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build();
} }