mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 08:55:44 +01:00
implement update function for plugins on frontend / adjust the plugin pending modal to show pending installations and updates
This commit is contained in:
@@ -4,6 +4,7 @@ import type {Collection, Links} from "./hal";
|
||||
export type Plugin = {
|
||||
name: string,
|
||||
version: string,
|
||||
newVersion: string,
|
||||
displayName: string,
|
||||
description?: string,
|
||||
author: string,
|
||||
@@ -24,3 +25,11 @@ export type PluginGroup = {
|
||||
name: string,
|
||||
plugins: Plugin[]
|
||||
};
|
||||
|
||||
export type PendingPlugins = {
|
||||
_links: Links,
|
||||
_embedded: {
|
||||
new: [],
|
||||
update: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export type { SubRepository, File } from "./Sources";
|
||||
|
||||
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";
|
||||
|
||||
|
||||
@@ -29,22 +29,32 @@
|
||||
"installedNavLink": "Installiert",
|
||||
"availableNavLink": "Verfügbar"
|
||||
},
|
||||
"installPending": "Austehende Plugins installieren",
|
||||
"executePending": "Austehende Plugin-Änderungen ausführen",
|
||||
"noPlugins": "Keine Plugins gefunden.",
|
||||
"modal": {
|
||||
"title": "{{name}} Plugin installieren",
|
||||
"title": {
|
||||
"install": "{{name}} Plugin installieren",
|
||||
"update": "{{name}} Plugin aktualisieren"
|
||||
},
|
||||
"restart": "Neustarten um Plugin zu aktivieren",
|
||||
"install": "Installieren",
|
||||
"update": "Aktualisieren",
|
||||
"installQueue": "Werden installiert:",
|
||||
"updateQueue": "Werden aktualisiert:",
|
||||
"installAndRestart": "Installieren und Neustarten",
|
||||
"updateAndRestart": "Aktualisieren und Neustarten",
|
||||
"executeAndRestart": "Ausführen und Neustarten",
|
||||
"abort": "Abbrechen",
|
||||
"author": "Autor",
|
||||
"version": "Version",
|
||||
"currentVersion": "Installierte Version",
|
||||
"newVersion": "Neue Version",
|
||||
"dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installiert, wenn sie noch nicht vorhanden sind!",
|
||||
"dependencies": "Abhängigkeiten",
|
||||
"successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
|
||||
"reload": "jetzt neu laden",
|
||||
"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": {
|
||||
|
||||
@@ -29,22 +29,32 @@
|
||||
"installedNavLink": "Installed",
|
||||
"availableNavLink": "Available"
|
||||
},
|
||||
"installPending": "Install pending plugins",
|
||||
"executePending": "Execute pending plugin changes",
|
||||
"noPlugins": "No plugins found.",
|
||||
"modal": {
|
||||
"title": "Install {{name}} Plugin",
|
||||
"title": {
|
||||
"install": "Install {{name}} Plugin",
|
||||
"update": "Update {{name}} Plugin"
|
||||
},
|
||||
"restart": "Restart to activate",
|
||||
"install": "Install",
|
||||
"update": "Update",
|
||||
"installQueue": "Will be installed:",
|
||||
"updateQueue": "Will be updated:",
|
||||
"installAndRestart": "Install and Restart",
|
||||
"updateAndRestart": "Update and Restart",
|
||||
"executeAndRestart": "Execute and Restart",
|
||||
"abort": "Abort",
|
||||
"author": "Author",
|
||||
"version": "Version",
|
||||
"currentVersion": "Installed version",
|
||||
"newVersion": "New version",
|
||||
"dependencyNotification": "With this plugin, the following dependencies will be installed if they are not available yet!",
|
||||
"dependencies": "Dependencies",
|
||||
"successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:",
|
||||
"reload": "reload now",
|
||||
"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": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
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 InstallPendingModal from "./InstallPendingModal";
|
||||
import ExecutePendingModal from "./ExecutePendingModal";
|
||||
|
||||
type Props = {
|
||||
collection: PluginCollection,
|
||||
pendingPlugins: PendingPlugins,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
@@ -16,7 +16,7 @@ type State = {
|
||||
showModal: boolean
|
||||
};
|
||||
|
||||
class InstallPendingAction extends React.Component<Props, State> {
|
||||
class ExecutePendingAction extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -38,11 +38,11 @@ class InstallPendingAction extends React.Component<Props, State> {
|
||||
|
||||
renderModal = () => {
|
||||
const { showModal } = this.state;
|
||||
const { collection } = this.props;
|
||||
const { pendingPlugins } = this.props;
|
||||
if (showModal) {
|
||||
return (
|
||||
<InstallPendingModal
|
||||
collection={collection}
|
||||
<ExecutePendingModal
|
||||
pendingPlugins={pendingPlugins}
|
||||
onClose={this.closeModal}
|
||||
/>
|
||||
);
|
||||
@@ -57,7 +57,7 @@ class InstallPendingAction extends React.Component<Props, State> {
|
||||
{this.renderModal()}
|
||||
<Button
|
||||
color="primary"
|
||||
label={t("plugins.installPending")}
|
||||
label={t("plugins.executePending")}
|
||||
action={this.openModal}
|
||||
/>
|
||||
</>
|
||||
@@ -65,4 +65,4 @@ class InstallPendingAction extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("admin")(InstallPendingAction);
|
||||
export default translate("admin")(ExecutePendingAction);
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
Modal,
|
||||
Notification
|
||||
} 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 waitForRestart from "./waitForRestart";
|
||||
import InstallSuccessNotification from "./InstallSuccessNotification";
|
||||
import InstallSuccessNotification from "./SuccessNotification";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
collection: PluginCollection,
|
||||
pendingPlugins: PendingPlugins,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
@@ -27,7 +27,7 @@ type State = {
|
||||
error?: Error
|
||||
};
|
||||
|
||||
class InstallPendingModal extends React.Component<Props, State> {
|
||||
class ExecutePendingModal extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -53,13 +53,13 @@ class InstallPendingModal extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
installAndRestart = () => {
|
||||
const { collection } = this.props;
|
||||
const { pendingPlugins } = this.props;
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
apiClient
|
||||
.post(collection._links.installPending.href)
|
||||
.post(pendingPlugins._links.execute.href)
|
||||
.then(waitForRestart)
|
||||
.then(() => {
|
||||
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 = () => {
|
||||
const { collection, t } = this.props;
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="media">
|
||||
<div className="content">
|
||||
<p>{t("plugins.modal.installPending")}</p>
|
||||
<ul>
|
||||
{collection._embedded.plugins
|
||||
.filter(plugin => plugin.pending)
|
||||
.map(plugin => (
|
||||
<li key={plugin.name} className="has-text-weight-bold">
|
||||
{plugin.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>{t("plugins.modal.executePending")}</p>
|
||||
{this.renderInstallQueue()}
|
||||
{this.renderUpdateQueue()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="media">{this.renderNotifications()}</div>
|
||||
@@ -107,7 +142,7 @@ class InstallPendingModal extends React.Component<Props, State> {
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="warning"
|
||||
label={t("plugins.modal.installAndRestart")}
|
||||
label={t("plugins.modal.executeAndRestart")}
|
||||
loading={loading}
|
||||
action={this.installAndRestart}
|
||||
disabled={error || success}
|
||||
@@ -121,7 +156,7 @@ class InstallPendingModal extends React.Component<Props, State> {
|
||||
const { onClose, t } = this.props;
|
||||
return (
|
||||
<Modal
|
||||
title={t("plugins.modal.installAndRestart")}
|
||||
title={t("plugins.modal.executeAndRestart")}
|
||||
closeFunction={onClose}
|
||||
body={this.renderBody()}
|
||||
footer={this.renderFooter()}
|
||||
@@ -131,4 +166,4 @@ class InstallPendingModal extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("admin")(InstallPendingModal);
|
||||
export default translate("admin")(ExecutePendingModal);
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@scm-manager/ui-components";
|
||||
import classNames from "classnames";
|
||||
import waitForRestart from "./waitForRestart";
|
||||
import InstallSuccessNotification from "./InstallSuccessNotification";
|
||||
import SuccessNotification from "./SuccessNotification";
|
||||
|
||||
type Props = {
|
||||
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) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -162,7 +162,7 @@ class PluginModal extends React.Component<Props, State> {
|
||||
} else if (success) {
|
||||
return (
|
||||
<div className="media">
|
||||
<InstallSuccessNotification />
|
||||
<SuccessNotification />
|
||||
</div>
|
||||
);
|
||||
} else if (restart) {
|
||||
@@ -252,7 +252,7 @@ class PluginModal extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("plugins.modal.title", {
|
||||
title={t("plugins.modal.title.install", {
|
||||
name: plugin.displayName ? plugin.displayName : plugin.name
|
||||
})}
|
||||
closeFunction={() => onClose()}
|
||||
@@ -267,4 +267,4 @@ class PluginModal extends React.Component<Props, State> {
|
||||
export default compose(
|
||||
injectSheet(styles),
|
||||
translate("admin")
|
||||
)(PluginModal);
|
||||
)(InstallPluginModal);
|
||||
@@ -4,8 +4,9 @@ import injectSheet from "react-jss";
|
||||
import type { Plugin } from "@scm-manager/ui-types";
|
||||
import { CardColumn } from "@scm-manager/ui-components";
|
||||
import PluginAvatar from "./PluginAvatar";
|
||||
import PluginModal from "./PluginModal";
|
||||
import classNames from "classnames";
|
||||
import InstallPluginModal from "./InstallPluginModal";
|
||||
import UpdatePluginModal from "./UpdatePluginModal";
|
||||
|
||||
type Props = {
|
||||
plugin: Plugin,
|
||||
@@ -22,9 +23,15 @@ type State = {
|
||||
const styles = {
|
||||
link: {
|
||||
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",
|
||||
right: 0,
|
||||
top: 0
|
||||
@@ -59,17 +66,52 @@ class PluginEntry extends React.Component<Props, State> {
|
||||
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;
|
||||
if (this.isInstallable()) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(classes.link, "level-item")}
|
||||
className={classNames(classes.link, classes.topRight, "level-item")}
|
||||
onClick={this.toggleModal}
|
||||
>
|
||||
<i className="fas fa-download has-text-info" />
|
||||
</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;
|
||||
if (plugin.pending) {
|
||||
return (
|
||||
<span className={classes.spinner}>
|
||||
<span className={classes.topRight}>
|
||||
<i className="fas fa-spinner fa-spin has-text-info" />
|
||||
</span>
|
||||
);
|
||||
@@ -86,19 +128,13 @@ class PluginEntry extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { plugin, refresh } = this.props;
|
||||
const { plugin } = this.props;
|
||||
const { showModal } = this.state;
|
||||
const avatar = this.createAvatar(plugin);
|
||||
const footerLeft = this.createFooterLeft();
|
||||
const actionbar = this.createActionbar();
|
||||
const footerRight = this.createFooterRight(plugin);
|
||||
|
||||
const modal = showModal ? (
|
||||
<PluginModal
|
||||
plugin={plugin}
|
||||
refresh={refresh}
|
||||
onClose={this.toggleModal}
|
||||
/>
|
||||
) : null;
|
||||
const modal = showModal ? this.renderModal() : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -107,8 +143,9 @@ class PluginEntry extends React.Component<Props, State> {
|
||||
avatar={avatar}
|
||||
title={plugin.displayName ? plugin.displayName : plugin.name}
|
||||
description={plugin.description}
|
||||
contentRight={this.createPendingSpinner()}
|
||||
footerLeft={footerLeft}
|
||||
contentRight={
|
||||
plugin.pending ? this.createPendingSpinner() : actionbar
|
||||
}
|
||||
footerRight={footerRight}
|
||||
/>
|
||||
{modal}
|
||||
|
||||
288
scm-ui/src/admin/plugins/components/UpdatePluginModal.js
Normal file
288
scm-ui/src/admin/plugins/components/UpdatePluginModal.js
Normal 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);
|
||||
@@ -3,28 +3,31 @@ import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { compose } from "redux";
|
||||
import type { PluginCollection } from "@scm-manager/ui-types";
|
||||
import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
|
||||
import {
|
||||
ErrorNotification,
|
||||
Loading,
|
||||
Title,
|
||||
Subtitle,
|
||||
Notification,
|
||||
ErrorNotification
|
||||
Subtitle,
|
||||
Title
|
||||
} from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchPendingPlugins,
|
||||
fetchPluginsByLink,
|
||||
getFetchPluginsFailure,
|
||||
getPendingPlugins,
|
||||
getPluginCollection,
|
||||
isFetchPluginsPending
|
||||
} from "../modules/plugins";
|
||||
import PluginsList from "../components/PluginList";
|
||||
import {
|
||||
getAvailablePluginsLink,
|
||||
getInstalledPluginsLink
|
||||
getInstalledPluginsLink,
|
||||
getPendingPluginsLink
|
||||
} from "../../../modules/indexResource";
|
||||
import PluginTopActions from "../components/PluginTopActions";
|
||||
import PluginBottomActions from "../components/PluginBottomActions";
|
||||
import InstallPendingAction from "../components/InstallPendingAction";
|
||||
import ExecutePendingAction from "../components/ExecutePendingAction";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
@@ -34,12 +37,15 @@ type Props = {
|
||||
installed: boolean,
|
||||
availablePluginsLink: string,
|
||||
installedPluginsLink: string,
|
||||
pendingPluginsLink: string,
|
||||
pendingPlugins: PendingPlugins,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
|
||||
// dispatched functions
|
||||
fetchPluginsByLink: (link: string) => void
|
||||
fetchPluginsByLink: (link: string) => void,
|
||||
fetchPendingPlugins: (link: string) => void
|
||||
};
|
||||
|
||||
class PluginsOverview extends React.Component<Props> {
|
||||
@@ -48,15 +54,16 @@ class PluginsOverview extends React.Component<Props> {
|
||||
installed,
|
||||
fetchPluginsByLink,
|
||||
availablePluginsLink,
|
||||
installedPluginsLink
|
||||
installedPluginsLink,
|
||||
pendingPluginsLink,
|
||||
fetchPendingPlugins
|
||||
} = this.props;
|
||||
fetchPluginsByLink(installed ? installedPluginsLink : availablePluginsLink);
|
||||
fetchPendingPlugins(pendingPluginsLink);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
installed,
|
||||
} = this.props;
|
||||
const { installed } = this.props;
|
||||
if (prevProps.installed !== installed) {
|
||||
this.fetchPlugins();
|
||||
}
|
||||
@@ -67,11 +74,12 @@ class PluginsOverview extends React.Component<Props> {
|
||||
installed,
|
||||
fetchPluginsByLink,
|
||||
availablePluginsLink,
|
||||
installedPluginsLink
|
||||
installedPluginsLink,
|
||||
pendingPluginsLink,
|
||||
fetchPendingPlugins
|
||||
} = this.props;
|
||||
fetchPluginsByLink(
|
||||
installed ? installedPluginsLink : availablePluginsLink
|
||||
);
|
||||
fetchPluginsByLink(installed ? installedPluginsLink : availablePluginsLink);
|
||||
fetchPendingPlugins(pendingPluginsLink);
|
||||
};
|
||||
|
||||
renderHeader = (actions: React.Node) => {
|
||||
@@ -101,9 +109,13 @@ class PluginsOverview extends React.Component<Props> {
|
||||
};
|
||||
|
||||
createActions = () => {
|
||||
const { collection } = this.props;
|
||||
if (collection._links.installPending) {
|
||||
return <InstallPendingAction collection={collection} />;
|
||||
const { pendingPlugins } = this.props;
|
||||
if (
|
||||
pendingPlugins &&
|
||||
pendingPlugins._links &&
|
||||
pendingPlugins._links.execute
|
||||
) {
|
||||
return <ExecutePendingAction pendingPlugins={pendingPlugins} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -134,7 +146,12 @@ class PluginsOverview extends React.Component<Props> {
|
||||
const { collection, t } = this.props;
|
||||
|
||||
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>;
|
||||
}
|
||||
@@ -146,13 +163,17 @@ const mapStateToProps = state => {
|
||||
const error = getFetchPluginsFailure(state);
|
||||
const availablePluginsLink = getAvailablePluginsLink(state);
|
||||
const installedPluginsLink = getInstalledPluginsLink(state);
|
||||
const pendingPluginsLink = getPendingPluginsLink(state);
|
||||
const pendingPlugins = getPendingPlugins(state);
|
||||
|
||||
return {
|
||||
collection,
|
||||
loading,
|
||||
error,
|
||||
availablePluginsLink,
|
||||
installedPluginsLink
|
||||
installedPluginsLink,
|
||||
pendingPluginsLink,
|
||||
pendingPlugins
|
||||
};
|
||||
};
|
||||
|
||||
@@ -160,6 +181,9 @@ const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchPluginsByLink: (link: string) => {
|
||||
dispatch(fetchPluginsByLink(link));
|
||||
},
|
||||
fetchPendingPlugins: (link: string) => {
|
||||
dispatch(fetchPendingPlugins(link));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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_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) {
|
||||
@@ -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
|
||||
function normalizeByName(pluginCollection: PluginCollection) {
|
||||
function normalizeByName(state: Object, pluginCollection: PluginCollection) {
|
||||
const names = [];
|
||||
const byNames = {};
|
||||
for (const plugin of pluginCollection._embedded.plugins) {
|
||||
@@ -114,6 +161,7 @@ function normalizeByName(pluginCollection: PluginCollection) {
|
||||
byNames[plugin.name] = plugin;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
list: {
|
||||
...pluginCollection,
|
||||
_embedded: {
|
||||
@@ -144,9 +192,11 @@ export default function reducer(
|
||||
|
||||
switch (action.type) {
|
||||
case FETCH_PLUGINS_SUCCESS:
|
||||
return normalizeByName(action.payload);
|
||||
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;
|
||||
}
|
||||
@@ -189,3 +239,17 @@ export function isFetchPluginPending(state: Object, name: string) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -124,6 +124,10 @@ export function getInstalledPluginsLink(state: Object) {
|
||||
return getLink(state, "installedPlugins");
|
||||
}
|
||||
|
||||
export function getPendingPluginsLink(state: Object) {
|
||||
return getLink(state, "pendingPlugins");
|
||||
}
|
||||
|
||||
export function getMeLink(state: Object) {
|
||||
return getLink(state, "me");
|
||||
}
|
||||
|
||||
@@ -64,13 +64,16 @@ public class PendingPluginResource {
|
||||
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self());
|
||||
|
||||
if (!pending.isEmpty()) {
|
||||
linksBuilder.single(link("install", resourceLinks.pendingPluginCollection().installPending()));
|
||||
List<PluginDto> newPluginDtos = newPlugins.map(mapper::mapAvailable).collect(toList());
|
||||
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.with("new", newPlugins.map(mapper::mapAvailable).collect(toList()));
|
||||
embedded.with("update", updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()));
|
||||
embedded.with("new", newPluginDtos);
|
||||
embedded.with("update", updatePluginDtos);
|
||||
|
||||
return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user