mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 00:45:44 +01:00
Merged in feature/abort_plugin_installation (pull request #326)
Feature/abort plugin installation
This commit is contained in:
@@ -5,7 +5,7 @@ import com.google.common.base.Preconditions;
|
|||||||
public class AvailablePlugin implements Plugin {
|
public class AvailablePlugin implements Plugin {
|
||||||
|
|
||||||
private final AvailablePluginDescriptor pluginDescriptor;
|
private final AvailablePluginDescriptor pluginDescriptor;
|
||||||
private final boolean pending;
|
private boolean pending;
|
||||||
|
|
||||||
public AvailablePlugin(AvailablePluginDescriptor pluginDescriptor) {
|
public AvailablePlugin(AvailablePluginDescriptor pluginDescriptor) {
|
||||||
this(pluginDescriptor, false);
|
this(pluginDescriptor, false);
|
||||||
@@ -25,6 +25,10 @@ public class AvailablePlugin implements Plugin {
|
|||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void cancelInstallation() {
|
||||||
|
this.pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
public AvailablePlugin install() {
|
public AvailablePlugin install() {
|
||||||
Preconditions.checkState(!pending, "installation is already pending");
|
Preconditions.checkState(!pending, "installation is already pending");
|
||||||
return new AvailablePlugin(pluginDescriptor, true);
|
return new AvailablePlugin(pluginDescriptor, true);
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ public interface PluginManager {
|
|||||||
*/
|
*/
|
||||||
List<AvailablePlugin> getAvailable();
|
List<AvailablePlugin> getAvailable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all updatable plugins.
|
||||||
|
*
|
||||||
|
* @return a list of updatable plugins.
|
||||||
|
*/
|
||||||
|
List<InstalledPlugin> getUpdatable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs the plugin with the given name from the list of available plugins.
|
* Installs the plugin with the given name from the list of available plugins.
|
||||||
*
|
*
|
||||||
@@ -93,4 +100,14 @@ public interface PluginManager {
|
|||||||
* Install all pending plugins and restart the scm context.
|
* Install all pending plugins and restart the scm context.
|
||||||
*/
|
*/
|
||||||
void executePendingAndRestart();
|
void executePendingAndRestart();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel all pending plugins.
|
||||||
|
*/
|
||||||
|
void cancelPending();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all installed plugins.
|
||||||
|
*/
|
||||||
|
void updateAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class ButtonGroup extends React.Component<Props> {
|
|||||||
const childWrapper = [];
|
const childWrapper = [];
|
||||||
React.Children.forEach(children, child => {
|
React.Children.forEach(children, child => {
|
||||||
if (child) {
|
if (child) {
|
||||||
childWrapper.push(<p className="control" key={childWrapper.length}>{child}</p>);
|
childWrapper.push(<div className="control" key={childWrapper.length}>{child}</div>);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type Plugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PluginCollection = Collection & {
|
export type PluginCollection = Collection & {
|
||||||
|
_links: Links,
|
||||||
_embedded: {
|
_embedded: {
|
||||||
plugins: Plugin[] | string[]
|
plugins: Plugin[] | string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,11 @@
|
|||||||
"installedNavLink": "Installiert",
|
"installedNavLink": "Installiert",
|
||||||
"availableNavLink": "Verfügbar"
|
"availableNavLink": "Verfügbar"
|
||||||
},
|
},
|
||||||
"executePending": "Ausstehende Plugin-Änderungen ausführen",
|
"executePending": "Änderungen ausführen",
|
||||||
|
"outdatedPlugins": "{{count}} veraltetes Plugin",
|
||||||
|
"outdatedPlugins_plural": "{{count}} veraltete Plugins",
|
||||||
|
"updateAll": "Alle Plugins aktualisieren",
|
||||||
|
"cancelPending": "Änderungen abbrechen",
|
||||||
"noPlugins": "Keine Plugins gefunden.",
|
"noPlugins": "Keine Plugins gefunden.",
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": {
|
"title": {
|
||||||
@@ -48,6 +52,7 @@
|
|||||||
"updateAndRestart": "Aktualisieren und Neustarten",
|
"updateAndRestart": "Aktualisieren und Neustarten",
|
||||||
"uninstallAndRestart": "Deinstallieren and Neustarten",
|
"uninstallAndRestart": "Deinstallieren and Neustarten",
|
||||||
"executeAndRestart": "Ausführen und Neustarten",
|
"executeAndRestart": "Ausführen und Neustarten",
|
||||||
|
"updateAll": "Alle Plugins aktualisieren",
|
||||||
"abort": "Abbrechen",
|
"abort": "Abbrechen",
|
||||||
"author": "Autor",
|
"author": "Autor",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
@@ -58,7 +63,9 @@
|
|||||||
"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.",
|
||||||
"executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. 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.",
|
||||||
|
"cancelPending": "Die folgenden Plugin-Änderungen werden abgebrochen und zurückgesetzt.",
|
||||||
|
"updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repositoryRole": {
|
"repositoryRole": {
|
||||||
|
|||||||
@@ -29,7 +29,11 @@
|
|||||||
"installedNavLink": "Installed",
|
"installedNavLink": "Installed",
|
||||||
"availableNavLink": "Available"
|
"availableNavLink": "Available"
|
||||||
},
|
},
|
||||||
"executePending": "Execute pending plugin changes",
|
"executePending": "Execute changes",
|
||||||
|
"outdatedPlugins": "{{count}} outdated plugin",
|
||||||
|
"outdatedPlugins_plural": "{{count}} outdated plugins",
|
||||||
|
"updateAll": "Update all plugins",
|
||||||
|
"cancelPending": "Cancel changes",
|
||||||
"noPlugins": "No plugins found.",
|
"noPlugins": "No plugins found.",
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": {
|
"title": {
|
||||||
@@ -48,6 +52,7 @@
|
|||||||
"updateAndRestart": "Update and Restart",
|
"updateAndRestart": "Update and Restart",
|
||||||
"uninstallAndRestart": "Uninstall and Restart",
|
"uninstallAndRestart": "Uninstall and Restart",
|
||||||
"executeAndRestart": "Execute and Restart",
|
"executeAndRestart": "Execute and Restart",
|
||||||
|
"updateAll": "Update all plugins",
|
||||||
"abort": "Abort",
|
"abort": "Abort",
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
@@ -58,7 +63,9 @@
|
|||||||
"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.",
|
||||||
"executePending": "The following plugin changes will be executed and after that 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.",
|
||||||
|
"cancelPending": "The following plugin changes will be canceled.",
|
||||||
|
"updateAllInfo": "The following plugin changes will be executed. You need to restart the scm-manager to make these changes effective."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repositoryRole": {
|
"repositoryRole": {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
"availableNavLink": "Disponibles"
|
"availableNavLink": "Disponibles"
|
||||||
},
|
},
|
||||||
"executePending": "Ejecutar los complementos pendientes",
|
"executePending": "Ejecutar los complementos pendientes",
|
||||||
|
"updateAll": "Actualizar todos los complementos",
|
||||||
|
"cancelPending": "Cancelar los complementos pendientes",
|
||||||
"noPlugins": "No se han encontrado complementos.",
|
"noPlugins": "No se han encontrado complementos.",
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": {
|
"title": {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PluginActionModal from "./PluginActionModal";
|
||||||
|
import type { PendingPlugins } from "@scm-manager/ui-types";
|
||||||
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
refresh: () => void,
|
||||||
|
pendingPlugins: PendingPlugins,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class CancelPendingActionModal extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { onClose, pendingPlugins, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PluginActionModal
|
||||||
|
description={t("plugins.modal.cancelPending")}
|
||||||
|
label={t("plugins.cancelPending")}
|
||||||
|
onClose={onClose}
|
||||||
|
pendingPlugins={pendingPlugins}
|
||||||
|
execute={this.cancelPending}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelPending = () => {
|
||||||
|
const { pendingPlugins, refresh, onClose } = this.props;
|
||||||
|
return apiClient
|
||||||
|
.post(pendingPlugins._links.cancel.href)
|
||||||
|
.then(refresh)
|
||||||
|
.then(onClose);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("admin")(CancelPendingActionModal);
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@scm-manager/ui-components";
|
|
||||||
import type { PendingPlugins } from "@scm-manager/ui-types";
|
|
||||||
import { translate } from "react-i18next";
|
|
||||||
import ExecutePendingModal from "./ExecutePendingModal";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pendingPlugins: PendingPlugins,
|
|
||||||
|
|
||||||
// context props
|
|
||||||
t: string => string
|
|
||||||
};
|
|
||||||
|
|
||||||
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 translate("admin")(ExecutePendingAction);
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PluginActionModal from "./PluginActionModal";
|
||||||
|
import type { PendingPlugins } from "@scm-manager/ui-types";
|
||||||
|
import waitForRestart from "./waitForRestart";
|
||||||
|
import { apiClient, Notification } from "@scm-manager/ui-components";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
pendingPlugins: PendingPlugins,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExecutePendingActionModal extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { onClose, pendingPlugins, t } = this.props;
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeAndRestart = () => {
|
||||||
|
const { pendingPlugins } = this.props;
|
||||||
|
return apiClient
|
||||||
|
.post(pendingPlugins._links.execute.href)
|
||||||
|
.then(waitForRestart);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("admin")(ExecutePendingActionModal);
|
||||||
@@ -1,21 +1,26 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
apiClient,
|
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
ErrorNotification,
|
ErrorNotification,
|
||||||
Modal,
|
Modal
|
||||||
Notification
|
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import type { PendingPlugins } from "@scm-manager/ui-types";
|
import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import waitForRestart from "./waitForRestart";
|
|
||||||
import SuccessNotification from "./SuccessNotification";
|
import SuccessNotification from "./SuccessNotification";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
pendingPlugins: PendingPlugins,
|
actionType: string,
|
||||||
|
pendingPlugins?: PendingPlugins,
|
||||||
|
installedPlugins?: PluginCollection,
|
||||||
|
refresh: () => void,
|
||||||
|
execute: () => Promise<any>,
|
||||||
|
description: string,
|
||||||
|
label: string,
|
||||||
|
|
||||||
|
children?: React.Node,
|
||||||
|
|
||||||
// context props
|
// context props
|
||||||
t: string => string
|
t: string => string
|
||||||
@@ -27,7 +32,7 @@ type State = {
|
|||||||
error?: Error
|
error?: Error
|
||||||
};
|
};
|
||||||
|
|
||||||
class ExecutePendingModal extends React.Component<Props, State> {
|
class PluginActionModal extends React.Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -37,35 +42,28 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderNotifications = () => {
|
renderNotifications = () => {
|
||||||
const { t } = this.props;
|
const { children } = this.props;
|
||||||
const { error, success } = this.state;
|
const { error, success } = this.state;
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorNotification error={error} />;
|
return <ErrorNotification error={error} />;
|
||||||
} else if (success) {
|
} else if (success) {
|
||||||
return <SuccessNotification />;
|
return <SuccessNotification />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return children;
|
||||||
<Notification type="warning">
|
|
||||||
{t("plugins.modal.restartNotification")}
|
|
||||||
</Notification>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
executeAndRestart = () => {
|
executeAction = () => {
|
||||||
const { pendingPlugins } = this.props;
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true
|
loading: true
|
||||||
});
|
});
|
||||||
|
|
||||||
apiClient
|
this.props
|
||||||
.post(pendingPlugins._links.execute.href)
|
.execute()
|
||||||
.then(waitForRestart)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
success: true,
|
success: true,
|
||||||
loading: false,
|
loading: false
|
||||||
error: undefined
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -77,11 +75,45 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderModalContent = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderUpdatable()}
|
||||||
|
{this.renderInstallQueue()}
|
||||||
|
{this.renderUpdateQueue()}
|
||||||
|
{this.renderUninstallQueue()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderUpdatable = () => {
|
||||||
|
const { installedPlugins, t } = this.props;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{installedPlugins &&
|
||||||
|
installedPlugins._embedded &&
|
||||||
|
installedPlugins._embedded.plugins && (
|
||||||
|
<>
|
||||||
|
<strong>{t("plugins.modal.updateQueue")}</strong>
|
||||||
|
<ul>
|
||||||
|
{installedPlugins._embedded.plugins
|
||||||
|
.filter(plugin => plugin._links && plugin._links.update)
|
||||||
|
.map(plugin => (
|
||||||
|
<li key={plugin.name}>{plugin.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderInstallQueue = () => {
|
renderInstallQueue = () => {
|
||||||
const { pendingPlugins, t } = this.props;
|
const { pendingPlugins, t } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{pendingPlugins._embedded &&
|
{pendingPlugins &&
|
||||||
|
pendingPlugins._embedded &&
|
||||||
pendingPlugins._embedded.new.length > 0 && (
|
pendingPlugins._embedded.new.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<strong>{t("plugins.modal.installQueue")}</strong>
|
<strong>{t("plugins.modal.installQueue")}</strong>
|
||||||
@@ -100,7 +132,8 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
const { pendingPlugins, t } = this.props;
|
const { pendingPlugins, t } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{pendingPlugins._embedded &&
|
{pendingPlugins &&
|
||||||
|
pendingPlugins._embedded &&
|
||||||
pendingPlugins._embedded.update.length > 0 && (
|
pendingPlugins._embedded.update.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<strong>{t("plugins.modal.updateQueue")}</strong>
|
<strong>{t("plugins.modal.updateQueue")}</strong>
|
||||||
@@ -119,7 +152,8 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
const { pendingPlugins, t } = this.props;
|
const { pendingPlugins, t } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{pendingPlugins._embedded &&
|
{pendingPlugins &&
|
||||||
|
pendingPlugins._embedded &&
|
||||||
pendingPlugins._embedded.uninstall.length > 0 && (
|
pendingPlugins._embedded.uninstall.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<strong>{t("plugins.modal.uninstallQueue")}</strong>
|
<strong>{t("plugins.modal.uninstallQueue")}</strong>
|
||||||
@@ -135,15 +169,12 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderBody = () => {
|
renderBody = () => {
|
||||||
const { t } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="media">
|
<div className="media">
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<p>{t("plugins.modal.executePending")}</p>
|
<p>{this.props.description}</p>
|
||||||
{this.renderInstallQueue()}
|
{this.renderModalContent()}
|
||||||
{this.renderUpdateQueue()}
|
|
||||||
{this.renderUninstallQueue()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="media">{this.renderNotifications()}</div>
|
<div className="media">{this.renderNotifications()}</div>
|
||||||
@@ -158,9 +189,9 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
label={t("plugins.modal.executeAndRestart")}
|
label={this.props.label}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
action={this.executeAndRestart}
|
action={this.executeAction}
|
||||||
disabled={error || success}
|
disabled={error || success}
|
||||||
/>
|
/>
|
||||||
<Button label={t("plugins.modal.abort")} action={onClose} />
|
<Button label={t("plugins.modal.abort")} action={onClose} />
|
||||||
@@ -169,10 +200,10 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onClose, t } = this.props;
|
const { onClose } = this.props;
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t("plugins.modal.executeAndRestart")}
|
title={this.props.label}
|
||||||
closeFunction={onClose}
|
closeFunction={onClose}
|
||||||
body={this.renderBody()}
|
body={this.renderBody()}
|
||||||
footer={this.renderFooter()}
|
footer={this.renderFooter()}
|
||||||
@@ -182,4 +213,4 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate("admin")(ExecutePendingModal);
|
export default translate("admin")(PluginActionModal);
|
||||||
@@ -121,6 +121,7 @@ class PluginModal extends React.Component<Props, State> {
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
success: false,
|
||||||
error: error
|
error: error
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
42
scm-ui/src/admin/plugins/components/UpdateAllActionModal.js
Normal file
42
scm-ui/src/admin/plugins/components/UpdateAllActionModal.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PluginActionModal from "./PluginActionModal";
|
||||||
|
import type { PluginCollection } from "@scm-manager/ui-types";
|
||||||
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
refresh: () => void,
|
||||||
|
installedPlugins: PluginCollection,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class UpdateAllActionModal extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { onClose, installedPlugins, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PluginActionModal
|
||||||
|
description={t("plugins.modal.updateAll")}
|
||||||
|
label={t("plugins.updateAll")}
|
||||||
|
onClose={onClose}
|
||||||
|
installedPlugins={installedPlugins}
|
||||||
|
execute={this.updateAll}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAll = () => {
|
||||||
|
const { installedPlugins, refresh, onClose } = this.props;
|
||||||
|
return apiClient
|
||||||
|
.post(installedPlugins._links.update.href)
|
||||||
|
.then(refresh)
|
||||||
|
.then(onClose);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("admin")(UpdateAllActionModal);
|
||||||
@@ -5,11 +5,13 @@ import { translate } from "react-i18next";
|
|||||||
import { compose } from "redux";
|
import { compose } from "redux";
|
||||||
import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
|
import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
|
||||||
import {
|
import {
|
||||||
|
ButtonGroup,
|
||||||
ErrorNotification,
|
ErrorNotification,
|
||||||
Loading,
|
Loading,
|
||||||
Notification,
|
Notification,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
Title
|
Title,
|
||||||
|
Button
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import {
|
import {
|
||||||
fetchPendingPlugins,
|
fetchPendingPlugins,
|
||||||
@@ -27,7 +29,9 @@ import {
|
|||||||
} 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 ExecutePendingAction from "../components/ExecutePendingAction";
|
import ExecutePendingActionModal from "../components/ExecutePendingActionModal";
|
||||||
|
import CancelPendingActionModal from "../components/CancelPendingActionModal";
|
||||||
|
import UpdateAllActionModal from "../components/UpdateAllActionModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
@@ -41,14 +45,29 @@ type Props = {
|
|||||||
pendingPlugins: PendingPlugins,
|
pendingPlugins: PendingPlugins,
|
||||||
|
|
||||||
// context objects
|
// context objects
|
||||||
t: string => string,
|
t: (key: string, params?: Object) => string,
|
||||||
|
|
||||||
// dispatched functions
|
// dispatched functions
|
||||||
fetchPluginsByLink: (link: string) => void,
|
fetchPluginsByLink: (link: string) => void,
|
||||||
fetchPendingPlugins: (link: string) => void
|
fetchPendingPlugins: (link: string) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
class PluginsOverview extends React.Component<Props> {
|
type State = {
|
||||||
|
showPendingModal: boolean,
|
||||||
|
showUpdateAllModal: boolean,
|
||||||
|
showCancelModal: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class PluginsOverview extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props, context: *) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {
|
||||||
|
showPendingModal: false,
|
||||||
|
showUpdateAllModal: false,
|
||||||
|
showCancelModal: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.fetchPlugins();
|
this.fetchPlugins();
|
||||||
}
|
}
|
||||||
@@ -102,17 +121,72 @@ class PluginsOverview extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createActions = () => {
|
createActions = () => {
|
||||||
const { pendingPlugins } = this.props;
|
const { pendingPlugins, collection, t } = this.props;
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pendingPlugins &&
|
pendingPlugins &&
|
||||||
pendingPlugins._links &&
|
pendingPlugins._links &&
|
||||||
pendingPlugins._links.execute
|
pendingPlugins._links.execute
|
||||||
) {
|
) {
|
||||||
return <ExecutePendingAction pendingPlugins={pendingPlugins} />;
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
reducedMobile={true}
|
||||||
|
key={"executePending"}
|
||||||
|
icon={"arrow-circle-right"}
|
||||||
|
label={t("plugins.executePending")}
|
||||||
|
action={() => this.setState({ showPendingModal: true })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pendingPlugins &&
|
||||||
|
pendingPlugins._links &&
|
||||||
|
pendingPlugins._links.cancel
|
||||||
|
) {
|
||||||
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
reducedMobile={true}
|
||||||
|
key={"cancelPending"}
|
||||||
|
icon={"times"}
|
||||||
|
label={t("plugins.cancelPending")}
|
||||||
|
action={() => this.setState({ showCancelModal: true })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection && collection._links && collection._links.update) {
|
||||||
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
reducedMobile={true}
|
||||||
|
key={"updateAll"}
|
||||||
|
icon={"sync-alt"}
|
||||||
|
label={this.computeUpdateAllSize()}
|
||||||
|
action={() => this.setState({ showUpdateAllModal: true })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttons.length > 0) {
|
||||||
|
return <ButtonGroup>{buttons}</ButtonGroup>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
computeUpdateAllSize = () => {
|
||||||
|
const { collection, t } = this.props;
|
||||||
|
const outdatedPlugins = collection._embedded.plugins.filter(
|
||||||
|
p => p._links.update
|
||||||
|
).length;
|
||||||
|
return t("plugins.outdatedPlugins", {
|
||||||
|
count: outdatedPlugins
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, error, collection } = this.props;
|
const { loading, error, collection } = this.props;
|
||||||
|
|
||||||
@@ -131,10 +205,47 @@ class PluginsOverview extends React.Component<Props> {
|
|||||||
<hr className="header-with-actions" />
|
<hr className="header-with-actions" />
|
||||||
{this.renderPluginsList()}
|
{this.renderPluginsList()}
|
||||||
{this.renderFooter(actions)}
|
{this.renderFooter(actions)}
|
||||||
|
{this.renderModals()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderModals = () => {
|
||||||
|
const { collection, pendingPlugins } = this.props;
|
||||||
|
const {
|
||||||
|
showPendingModal,
|
||||||
|
showCancelModal,
|
||||||
|
showUpdateAllModal
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (showPendingModal) {
|
||||||
|
return (
|
||||||
|
<ExecutePendingActionModal
|
||||||
|
onClose={() => this.setState({ showPendingModal: false })}
|
||||||
|
pendingPlugins={pendingPlugins}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (showCancelModal) {
|
||||||
|
return (
|
||||||
|
<CancelPendingActionModal
|
||||||
|
onClose={() => this.setState({ showCancelModal: false })}
|
||||||
|
refresh={this.fetchPlugins}
|
||||||
|
pendingPlugins={pendingPlugins}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (showUpdateAllModal) {
|
||||||
|
return (
|
||||||
|
<UpdateAllActionModal
|
||||||
|
onClose={() => this.setState({ showUpdateAllModal: false })}
|
||||||
|
refresh={this.fetchPlugins}
|
||||||
|
installedPlugins={collection}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
renderPluginsList() {
|
renderPluginsList() {
|
||||||
const { collection, t } = this.props;
|
const { collection, t } = this.props;
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,21 @@ public class InstalledPluginResource {
|
|||||||
return Response.ok(collectionMapper.mapInstalled(plugins, available)).build();
|
return Response.ok(collectionMapper.mapInstalled(plugins, available)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all installed plugins.
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/update")
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 200, condition = "success"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
@TypeHint(CollectionDto.class)
|
||||||
|
public Response updateAll() {
|
||||||
|
pluginManager.updateAll();
|
||||||
|
return Response.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the installed plugin with the given id.
|
* Returns the installed plugin with the given id.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ public class PendingPluginResource {
|
|||||||
})
|
})
|
||||||
@Produces(VndMediaType.PLUGIN_COLLECTION)
|
@Produces(VndMediaType.PLUGIN_COLLECTION)
|
||||||
public Response getPending() {
|
public Response getPending() {
|
||||||
PluginPermissions.manage().check();
|
|
||||||
|
|
||||||
List<AvailablePlugin> pending = pluginManager
|
List<AvailablePlugin> pending = pluginManager
|
||||||
.getAvailable()
|
.getAvailable()
|
||||||
.stream()
|
.stream()
|
||||||
@@ -71,8 +69,12 @@ public class PendingPluginResource {
|
|||||||
List<PluginDto> updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
List<PluginDto> updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
||||||
List<PluginDto> uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
List<PluginDto> uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
||||||
|
|
||||||
if (!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty()) {
|
if (
|
||||||
|
PluginPermissions.manage().isPermitted() &&
|
||||||
|
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
|
||||||
|
) {
|
||||||
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
|
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
|
||||||
|
linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Embedded.Builder embedded = Embedded.embeddedBuilder();
|
Embedded.Builder embedded = Embedded.embeddedBuilder();
|
||||||
@@ -106,8 +108,18 @@ public class PendingPluginResource {
|
|||||||
@ResponseCode(code = 500, condition = "internal server error")
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
})
|
})
|
||||||
public Response executePending() {
|
public Response executePending() {
|
||||||
PluginPermissions.manage().check();
|
|
||||||
pluginManager.executePendingAndRestart();
|
pluginManager.executePendingAndRestart();
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/cancel")
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 200, condition = "success"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
public Response cancelPending() {
|
||||||
|
pluginManager.cancelPending();
|
||||||
|
return Response.ok().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package sonia.scm.api.v2.resources;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import de.otto.edison.hal.Embedded;
|
import de.otto.edison.hal.Embedded;
|
||||||
import de.otto.edison.hal.HalRepresentation;
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
import de.otto.edison.hal.Link;
|
|
||||||
import de.otto.edison.hal.Links;
|
import de.otto.edison.hal.Links;
|
||||||
import sonia.scm.plugin.AvailablePlugin;
|
import sonia.scm.plugin.AvailablePlugin;
|
||||||
import sonia.scm.plugin.InstalledPlugin;
|
import sonia.scm.plugin.InstalledPlugin;
|
||||||
import sonia.scm.plugin.PluginPermissions;
|
import sonia.scm.plugin.PluginManager;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
||||||
|
import static de.otto.edison.hal.Link.link;
|
||||||
import static de.otto.edison.hal.Links.linkingTo;
|
import static de.otto.edison.hal.Links.linkingTo;
|
||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
@@ -19,11 +19,13 @@ public class PluginDtoCollectionMapper {
|
|||||||
|
|
||||||
private final ResourceLinks resourceLinks;
|
private final ResourceLinks resourceLinks;
|
||||||
private final PluginDtoMapper mapper;
|
private final PluginDtoMapper mapper;
|
||||||
|
private final PluginManager manager;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper) {
|
public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper, PluginManager manager) {
|
||||||
this.resourceLinks = resourceLinks;
|
this.resourceLinks = resourceLinks;
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HalRepresentation mapInstalled(List<InstalledPlugin> plugins, List<AvailablePlugin> availablePlugins) {
|
public HalRepresentation mapInstalled(List<InstalledPlugin> plugins, List<AvailablePlugin> availablePlugins) {
|
||||||
@@ -44,6 +46,11 @@ public class PluginDtoCollectionMapper {
|
|||||||
|
|
||||||
Links.Builder linksBuilder = linkingTo()
|
Links.Builder linksBuilder = linkingTo()
|
||||||
.with(Links.linkingTo().self(baseUrl).build());
|
.with(Links.linkingTo().self(baseUrl).build());
|
||||||
|
|
||||||
|
if (!manager.getUpdatable().isEmpty()) {
|
||||||
|
linksBuilder.single(link("update", resourceLinks.installedPluginCollection().update()));
|
||||||
|
}
|
||||||
|
|
||||||
return linksBuilder.build();
|
return linksBuilder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -686,6 +686,10 @@ class ResourceLinks {
|
|||||||
String self() {
|
String self() {
|
||||||
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugins").parameters().href();
|
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugins").parameters().href();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String update() {
|
||||||
|
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("updateAll").parameters().href();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public AvailablePluginLinks availablePlugin() {
|
public AvailablePluginLinks availablePlugin() {
|
||||||
@@ -739,6 +743,10 @@ class ResourceLinks {
|
|||||||
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("executePending").parameters().href();
|
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("executePending").parameters().href();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String cancelPending() {
|
||||||
|
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("cancelPending").parameters().href();
|
||||||
|
}
|
||||||
|
|
||||||
String self() {
|
String self() {
|
||||||
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("getPending").parameters().href();
|
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("getPending").parameters().href();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ import sonia.scm.lifecycle.RestartEvent;
|
|||||||
import sonia.scm.version.Version;
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -72,7 +72,8 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
private final PluginLoader loader;
|
private final PluginLoader loader;
|
||||||
private final PluginCenter center;
|
private final PluginCenter center;
|
||||||
private final PluginInstaller installer;
|
private final PluginInstaller installer;
|
||||||
private final Collection<PendingPluginInstallation> pendingQueue = new ArrayList<>();
|
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
|
||||||
|
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
|
||||||
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
|
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -106,7 +107,7 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<AvailablePlugin> getPending(String name) {
|
private Optional<AvailablePlugin> getPending(String name) {
|
||||||
return pendingQueue
|
return pendingInstallQueue
|
||||||
.stream()
|
.stream()
|
||||||
.map(PendingPluginInstallation::getPlugin)
|
.map(PendingPluginInstallation::getPlugin)
|
||||||
.filter(filterByName(name))
|
.filter(filterByName(name))
|
||||||
@@ -138,6 +139,15 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<InstalledPlugin> getUpdatable() {
|
||||||
|
return getInstalled()
|
||||||
|
.stream()
|
||||||
|
.filter(p -> isUpdatable(p.getDescriptor().getInformation().getName()))
|
||||||
|
.filter(p -> !p.isMarkedForUninstall())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
private <T extends Plugin> Predicate<T> filterByName(String name) {
|
private <T extends Plugin> Predicate<T> filterByName(String name) {
|
||||||
return plugin -> name.equals(plugin.getDescriptor().getInformation().getName());
|
return plugin -> name.equals(plugin.getDescriptor().getInformation().getName());
|
||||||
}
|
}
|
||||||
@@ -179,7 +189,7 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
if (restartAfterInstallation) {
|
if (restartAfterInstallation) {
|
||||||
restart("plugin installation");
|
restart("plugin installation");
|
||||||
} else {
|
} else {
|
||||||
pendingQueue.addAll(pendingInstallations);
|
pendingInstallQueue.addAll(pendingInstallations);
|
||||||
updateMayUninstallFlag();
|
updateMayUninstallFlag();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,10 +202,7 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
.orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name)));
|
.orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name)));
|
||||||
doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore());
|
doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore());
|
||||||
|
|
||||||
dependencyTracker.removeInstalled(installed.getDescriptor());
|
markForUninstall(installed);
|
||||||
installed.setMarkedForUninstall(true);
|
|
||||||
|
|
||||||
createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME);
|
|
||||||
|
|
||||||
if (restartAfterInstallation) {
|
if (restartAfterInstallation) {
|
||||||
restart("plugin installation");
|
restart("plugin installation");
|
||||||
@@ -215,18 +222,22 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
&& dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName());
|
&& dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createMarkerFile(InstalledPlugin plugin, String markerFile) {
|
private void markForUninstall(InstalledPlugin plugin) {
|
||||||
|
dependencyTracker.removeInstalled(plugin.getDescriptor());
|
||||||
try {
|
try {
|
||||||
Files.createFile(plugin.getDirectory().resolve(markerFile));
|
Path file = Files.createFile(plugin.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME));
|
||||||
} catch (IOException e) {
|
pendingUninstallQueue.add(new PendingPluginUninstallation(plugin, file));
|
||||||
throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + markerFile, e);
|
plugin.setMarkedForUninstall(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
dependencyTracker.addInstalled(plugin.getDescriptor());
|
||||||
|
throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + InstalledPlugin.UNINSTALL_MARKER_FILENAME, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void executePendingAndRestart() {
|
public void executePendingAndRestart() {
|
||||||
PluginPermissions.manage().check();
|
PluginPermissions.manage().check();
|
||||||
if (!pendingQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
|
if (!pendingInstallQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
|
||||||
restart("execute pending plugin changes");
|
restart("execute pending plugin changes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,4 +280,25 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
private boolean isUpdatable(String name) {
|
private boolean isUpdatable(String name) {
|
||||||
return getAvailable(name).isPresent() && !getPending(name).isPresent();
|
return getAvailable(name).isPresent() && !getPending(name).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelPending() {
|
||||||
|
PluginPermissions.manage().check();
|
||||||
|
pendingUninstallQueue.forEach(PendingPluginUninstallation::cancel);
|
||||||
|
pendingInstallQueue.forEach(PendingPluginInstallation::cancel);
|
||||||
|
pendingUninstallQueue.clear();
|
||||||
|
pendingInstallQueue.clear();
|
||||||
|
updateMayUninstallFlag();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateAll() {
|
||||||
|
PluginPermissions.manage().check();
|
||||||
|
for (InstalledPlugin installedPlugin : getInstalled()) {
|
||||||
|
String pluginName = installedPlugin.getDescriptor().getInformation().getName();
|
||||||
|
if (isUpdatable(pluginName)) {
|
||||||
|
install(pluginName, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ class PendingPluginInstallation {
|
|||||||
LOG.info("cancel installation of plugin {}", name);
|
LOG.info("cancel installation of plugin {}", name);
|
||||||
try {
|
try {
|
||||||
Files.delete(file);
|
Files.delete(file);
|
||||||
|
plugin.cancelInstallation();
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new PluginFailedToCancelInstallationException("failed to cancel installation of plugin " + name, ex);
|
throw new PluginFailedToCancelInstallationException("failed to cancel plugin installation ", name, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
class PendingPluginUninstallation {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(PendingPluginUninstallation.class);
|
||||||
|
|
||||||
|
private final InstalledPlugin plugin;
|
||||||
|
private final Path uninstallFile;
|
||||||
|
|
||||||
|
PendingPluginUninstallation(InstalledPlugin plugin, Path uninstallFile) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.uninstallFile = uninstallFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
String name = plugin.getDescriptor().getInformation().getName();
|
||||||
|
LOG.info("cancel uninstallation of plugin {}", name);
|
||||||
|
try {
|
||||||
|
Files.delete(uninstallFile);
|
||||||
|
plugin.setMarkedForUninstall(false);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new PluginFailedToCancelInstallationException("failed to cancel uninstallation", name, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,10 @@ class PluginDependencyTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void removeDependency(String from, String to) {
|
private void removeDependency(String from, String to) {
|
||||||
plugins.get(to).remove(from);
|
Collection<String> dependencies = plugins.get(to);
|
||||||
|
if (dependencies == null) {
|
||||||
|
throw new NullPointerException("inverse dependencies not found for " + to);
|
||||||
|
}
|
||||||
|
dependencies.remove(from);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
package sonia.scm.plugin;
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
public class PluginFailedToCancelInstallationException extends RuntimeException {
|
import sonia.scm.ExceptionWithContext;
|
||||||
public PluginFailedToCancelInstallationException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
|
public class PluginFailedToCancelInstallationException extends ExceptionWithContext {
|
||||||
|
public PluginFailedToCancelInstallationException(String message, String name, Exception cause) {
|
||||||
|
super(entity("plugin", name).build(), message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCode() {
|
||||||
|
return "65RdZ5atX1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,10 @@
|
|||||||
"40RaYIeeR1": {
|
"40RaYIeeR1": {
|
||||||
"displayName": "Es wurden keine Änderungen durchgeführt",
|
"displayName": "Es wurden keine Änderungen durchgeführt",
|
||||||
"description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt werden."
|
"description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt werden."
|
||||||
|
},
|
||||||
|
"65RdZ5atX1": {
|
||||||
|
"displayName": "Fehler beim Löschen von Plugin-Dateien",
|
||||||
|
"description": "Einige Dateien für die Plugin-Deinstallation konnten nicht gelöscht werden. Dieses kann zu Inkonsistenzen führen, so dass der SCM-Manager nicht mehr korrekt starten kann. Bitte prüfen Sie die Logs und bereinigen Sie das Plugin-Verzeichnis des SCM-Managers manuell. Um die Installation eines Plugins abzubrechen, löschen Sie die zugehörige smp Datei aus dem Plugin-Verzeichnis. Um ein Entfernen eines Plugins zu verhindern, entfernen Sie die Datei namens 'uninstall' aus dem entsprechenden Verzeichnis des Plugins."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"namespaceStrategies": {
|
"namespaceStrategies": {
|
||||||
|
|||||||
@@ -175,6 +175,10 @@
|
|||||||
"40RaYIeeR1": {
|
"40RaYIeeR1": {
|
||||||
"displayName": "No changes were made",
|
"displayName": "No changes were made",
|
||||||
"description": "No changes were made to the files of the repository. Therefor no new commit could be created."
|
"description": "No changes were made to the files of the repository. Therefor no new commit could be created."
|
||||||
|
},
|
||||||
|
"65RdZ5atX1": {
|
||||||
|
"displayName": "Error removing plugin files",
|
||||||
|
"description": "Some files to cancel the plugin (un)installation could not be deleted. This can lead to inconsistencies so that the SCM-Manager cannot restart properly. Please check the logs and clean up the plugin folder manually. To cancel the installation of a plugin, remove the corresponding smp file. To cancel the uninstallation, remove the file named 'uninstall' inside the directory for this plugin."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"namespaceStrategies": {
|
"namespaceStrategies": {
|
||||||
|
|||||||
@@ -34,11 +34,8 @@ import static java.util.Collections.emptyList;
|
|||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doNothing;
|
|
||||||
import static org.mockito.Mockito.doThrow;
|
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -74,12 +71,12 @@ class PendingPluginResourceTest {
|
|||||||
void mockMapper() {
|
void mockMapper() {
|
||||||
lenient().when(mapper.mapAvailable(any())).thenAnswer(invocation -> {
|
lenient().when(mapper.mapAvailable(any())).thenAnswer(invocation -> {
|
||||||
PluginDto dto = new PluginDto();
|
PluginDto dto = new PluginDto();
|
||||||
dto.setName(((AvailablePlugin)invocation.getArgument(0)).getDescriptor().getInformation().getName());
|
dto.setName(((AvailablePlugin) invocation.getArgument(0)).getDescriptor().getInformation().getName());
|
||||||
return dto;
|
return dto;
|
||||||
});
|
});
|
||||||
lenient().when(mapper.mapInstalled(any(), any())).thenAnswer(invocation -> {
|
lenient().when(mapper.mapInstalled(any(), any())).thenAnswer(invocation -> {
|
||||||
PluginDto dto = new PluginDto();
|
PluginDto dto = new PluginDto();
|
||||||
dto.setName(((InstalledPlugin)invocation.getArgument(0)).getDescriptor().getInformation().getName());
|
dto.setName(((InstalledPlugin) invocation.getArgument(0)).getDescriptor().getInformation().getName());
|
||||||
return dto;
|
return dto;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,7 +87,7 @@ class PendingPluginResourceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void bindSubject() {
|
void bindSubject() {
|
||||||
ThreadContext.bind(subject);
|
ThreadContext.bind(subject);
|
||||||
doNothing().when(subject).checkPermission("plugin:manage");
|
lenient().when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@@ -113,7 +110,7 @@ class PendingPluginResourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGetPendingAvailablePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException {
|
void shouldGetPendingAvailablePluginListWithInstallAndCancelLink() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin");
|
AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin");
|
||||||
when(availablePlugin.isPending()).thenReturn(true);
|
when(availablePlugin.isPending()).thenReturn(true);
|
||||||
when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin));
|
when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin));
|
||||||
@@ -124,6 +121,7 @@ class PendingPluginResourceTest {
|
|||||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\"");
|
assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\"");
|
||||||
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
||||||
|
assertThat(response.getContentAsString()).contains("\"cancel\":{\"href\":\"/v2/plugins/pending/cancel\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -166,6 +164,17 @@ class PendingPluginResourceTest {
|
|||||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
verify(pluginManager).executePendingAndRestart();
|
verify(pluginManager).executePendingAndRestart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCancelPendingPlugins() throws URISyntaxException {
|
||||||
|
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/cancel");
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
|
verify(pluginManager).cancelPending();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@@ -174,7 +183,7 @@ class PendingPluginResourceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void bindSubject() {
|
void bindSubject() {
|
||||||
ThreadContext.bind(subject);
|
ThreadContext.bind(subject);
|
||||||
doThrow(new ShiroException()).when(subject).checkPermission("plugin:manage");
|
when(subject.isPermitted("plugin:manage")).thenReturn(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@@ -183,23 +192,18 @@ class PendingPluginResourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotListPendingPlugins() throws URISyntaxException {
|
void shouldGetPendingAvailablePluginListWithoutInstallAndCancelLink() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
|
AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin");
|
||||||
|
when(availablePlugin.isPending()).thenReturn(true);
|
||||||
|
when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin));
|
||||||
|
|
||||||
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending");
|
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending");
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
verify(pluginManager, never()).executePendingAndRestart();
|
assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\"");
|
||||||
}
|
assertThat(response.getContentAsString()).doesNotContain("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
||||||
|
assertThat(response.getContentAsString()).doesNotContain("\"cancel\":{\"href\":\"/v2/plugins/pending/cancel\"}");
|
||||||
@Test
|
|
||||||
void shouldNotExecutePendingPlugins() throws URISyntaxException {
|
|
||||||
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute");
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
verify(pluginManager, never()).executePendingAndRestart();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ import org.junit.jupiter.api.BeforeEach;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.plugin.AvailablePlugin;
|
import sonia.scm.plugin.AvailablePlugin;
|
||||||
import sonia.scm.plugin.AvailablePluginDescriptor;
|
import sonia.scm.plugin.AvailablePluginDescriptor;
|
||||||
import sonia.scm.plugin.InstalledPlugin;
|
import sonia.scm.plugin.InstalledPlugin;
|
||||||
import sonia.scm.plugin.InstalledPluginDescriptor;
|
import sonia.scm.plugin.InstalledPluginDescriptor;
|
||||||
import sonia.scm.plugin.PluginInformation;
|
import sonia.scm.plugin.PluginInformation;
|
||||||
|
import sonia.scm.plugin.PluginManager;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -34,6 +38,9 @@ class PluginDtoCollectionMapperTest {
|
|||||||
@InjectMocks
|
@InjectMocks
|
||||||
PluginDtoMapperImpl pluginDtoMapper;
|
PluginDtoMapperImpl pluginDtoMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
PluginManager manager;
|
||||||
|
|
||||||
Subject subject = mock(Subject.class);
|
Subject subject = mock(Subject.class);
|
||||||
ThreadState subjectThreadState = new SubjectThreadState(subject);
|
ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||||
|
|
||||||
@@ -43,6 +50,11 @@ class PluginDtoCollectionMapperTest {
|
|||||||
ThreadContext.bind(subject);
|
ThreadContext.bind(subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void mockPluginManager() {
|
||||||
|
lenient().when(manager.getUpdatable()).thenReturn(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
public void unbindSubject() {
|
public void unbindSubject() {
|
||||||
ThreadContext.unbindSubject();
|
ThreadContext.unbindSubject();
|
||||||
@@ -51,7 +63,7 @@ class PluginDtoCollectionMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() {
|
void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() {
|
||||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||||
|
|
||||||
HalRepresentation result = mapper.mapInstalled(
|
HalRepresentation result = mapper.mapInstalled(
|
||||||
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||||
@@ -66,7 +78,7 @@ class PluginDtoCollectionMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSetNewVersionInInstalledPluginWhenAvailableVersionIsNewer() {
|
void shouldSetNewVersionInInstalledPluginWhenAvailableVersionIsNewer() {
|
||||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper,manager);
|
||||||
|
|
||||||
HalRepresentation result = mapper.mapInstalled(
|
HalRepresentation result = mapper.mapInstalled(
|
||||||
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||||
@@ -80,7 +92,7 @@ class PluginDtoCollectionMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldNotAddInstallLinkForNewVersionWhenNotPermitted() {
|
void shouldNotAddInstallLinkForNewVersionWhenNotPermitted() {
|
||||||
when(subject.isPermitted("plugin:manage")).thenReturn(false);
|
when(subject.isPermitted("plugin:manage")).thenReturn(false);
|
||||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||||
|
|
||||||
HalRepresentation result = mapper.mapInstalled(
|
HalRepresentation result = mapper.mapInstalled(
|
||||||
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||||
@@ -93,7 +105,7 @@ class PluginDtoCollectionMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldNotAddInstallLinkForNewVersionWhenInstallationIsPending() {
|
void shouldNotAddInstallLinkForNewVersionWhenInstallationIsPending() {
|
||||||
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||||
|
|
||||||
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
|
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
|
||||||
when(availablePlugin.isPending()).thenReturn(true);
|
when(availablePlugin.isPending()).thenReturn(true);
|
||||||
@@ -108,7 +120,7 @@ class PluginDtoCollectionMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldAddInstallLinkForNewVersionWhenPermitted() {
|
void shouldAddInstallLinkForNewVersionWhenPermitted() {
|
||||||
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||||
|
|
||||||
HalRepresentation result = mapper.mapInstalled(
|
HalRepresentation result = mapper.mapInstalled(
|
||||||
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||||
@@ -121,7 +133,7 @@ class PluginDtoCollectionMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() {
|
void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() {
|
||||||
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||||
|
|
||||||
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
|
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
|
||||||
when(availablePlugin.isPending()).thenReturn(true);
|
when(availablePlugin.isPending()).thenReturn(true);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import sonia.scm.ScmConstraintViolationException;
|
|||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.event.ScmEventBus;
|
||||||
import sonia.scm.lifecycle.RestartEvent;
|
import sonia.scm.lifecycle.RestartEvent;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -30,11 +32,13 @@ import static java.util.Collections.singletonList;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.Mockito.any;
|
import static org.mockito.Mockito.any;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static sonia.scm.plugin.PluginTestHelper.createAvailable;
|
import static sonia.scm.plugin.PluginTestHelper.createAvailable;
|
||||||
@@ -358,6 +362,24 @@ class DefaultPluginManagerTest {
|
|||||||
verify(mailPlugin).setMarkedForUninstall(true);
|
verify(mailPlugin).setMarkedForUninstall(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotChangeStateWhenUninstallFileCouldNotBeCreated() {
|
||||||
|
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
|
||||||
|
InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin");
|
||||||
|
when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin"));
|
||||||
|
|
||||||
|
when(reviewPlugin.getDirectory()).thenThrow(new PluginException("when the file could not be written an exception like this is thrown"));
|
||||||
|
|
||||||
|
when(loader.getInstalledPlugins()).thenReturn(asList(mailPlugin, reviewPlugin));
|
||||||
|
|
||||||
|
manager.computeInstallationDependencies();
|
||||||
|
|
||||||
|
assertThrows(PluginException.class, () -> manager.uninstall("scm-review-plugin", false));
|
||||||
|
|
||||||
|
verify(mailPlugin, never()).setMarkedForUninstall(true);
|
||||||
|
assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) {
|
void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) {
|
||||||
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
|
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
|
||||||
@@ -427,6 +449,71 @@ class DefaultPluginManagerTest {
|
|||||||
|
|
||||||
verify(eventBus).post(any(RestartEvent.class));
|
verify(eventBus).post(any(RestartEvent.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUndoPendingInstallations(@TempDirectory.TempDir Path temp) throws IOException {
|
||||||
|
InstalledPlugin mailPlugin = createInstalled("scm-ssh-plugin");
|
||||||
|
Path mailPluginPath = temp.resolve("scm-mail-plugin");
|
||||||
|
Files.createDirectories(mailPluginPath);
|
||||||
|
when(mailPlugin.getDirectory()).thenReturn(mailPluginPath);
|
||||||
|
when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin));
|
||||||
|
ArgumentCaptor<Boolean> uninstallCaptor = ArgumentCaptor.forClass(Boolean.class);
|
||||||
|
doNothing().when(mailPlugin).setMarkedForUninstall(uninstallCaptor.capture());
|
||||||
|
|
||||||
|
AvailablePlugin git = createAvailable("scm-git-plugin");
|
||||||
|
when(center.getAvailable()).thenReturn(ImmutableSet.of(git));
|
||||||
|
PendingPluginInstallation gitPendingPluginInformation = mock(PendingPluginInstallation.class);
|
||||||
|
when(installer.install(git)).thenReturn(gitPendingPluginInformation);
|
||||||
|
|
||||||
|
manager.install("scm-git-plugin", false);
|
||||||
|
manager.uninstall("scm-ssh-plugin", false);
|
||||||
|
|
||||||
|
manager.cancelPending();
|
||||||
|
|
||||||
|
assertThat(mailPluginPath.resolve("uninstall")).doesNotExist();
|
||||||
|
verify(gitPendingPluginInformation).cancel();
|
||||||
|
Boolean lasUninstallMarkerSet = uninstallCaptor.getAllValues().get(uninstallCaptor.getAllValues().size() - 1);
|
||||||
|
assertThat(lasUninstallMarkerSet).isFalse();
|
||||||
|
|
||||||
|
Files.createFile(mailPluginPath.resolve("uninstall"));
|
||||||
|
|
||||||
|
manager.cancelPending();
|
||||||
|
verify(gitPendingPluginInformation, times(1)).cancel();
|
||||||
|
assertThat(mailPluginPath.resolve("uninstall")).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateAllPlugins() {
|
||||||
|
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
|
||||||
|
InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin");
|
||||||
|
|
||||||
|
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(mailPlugin, reviewPlugin));
|
||||||
|
|
||||||
|
AvailablePlugin newMailPlugin = createAvailable("scm-mail-plugin", "2.0.0");
|
||||||
|
AvailablePlugin newReviewPlugin = createAvailable("scm-review-plugin", "2.0.0");
|
||||||
|
|
||||||
|
when(center.getAvailable()).thenReturn(ImmutableSet.of(newMailPlugin, newReviewPlugin));
|
||||||
|
|
||||||
|
manager.updateAll();
|
||||||
|
|
||||||
|
verify(installer).install(newMailPlugin);
|
||||||
|
verify(installer).install(newReviewPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotUpdateToOldPluginVersions() {
|
||||||
|
InstalledPlugin scriptPlugin = createInstalled("scm-script-plugin");
|
||||||
|
|
||||||
|
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(scriptPlugin));
|
||||||
|
AvailablePlugin oldScriptPlugin = createAvailable("scm-script-plugin", "0.9");
|
||||||
|
|
||||||
|
when(center.getAvailable()).thenReturn(ImmutableSet.of(oldScriptPlugin));
|
||||||
|
|
||||||
|
manager.updateAll();
|
||||||
|
|
||||||
|
verify(installer, never()).install(oldScriptPlugin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@@ -482,5 +569,14 @@ class DefaultPluginManagerTest {
|
|||||||
assertThrows(AuthorizationException.class, () -> manager.executePendingAndRestart());
|
assertThrows(AuthorizationException.class, () -> manager.executePendingAndRestart());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowAuthorizationExceptionsForCancelPending() {
|
||||||
|
assertThrows(AuthorizationException.class, () -> manager.cancelPending());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowAuthorizationExceptionsForUpdateAll() {
|
||||||
|
assertThrows(AuthorizationException.class, () -> manager.updateAll());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ public class PluginTestHelper {
|
|||||||
return createAvailable(information);
|
return createAvailable(information);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AvailablePlugin createAvailable(String name, String version) {
|
||||||
|
PluginInformation information = new PluginInformation();
|
||||||
|
information.setName(name);
|
||||||
|
information.setVersion(version);
|
||||||
|
return createAvailable(information);
|
||||||
|
}
|
||||||
|
|
||||||
public static InstalledPlugin createInstalled(String name) {
|
public static InstalledPlugin createInstalled(String name) {
|
||||||
PluginInformation information = new PluginInformation();
|
PluginInformation information = new PluginInformation();
|
||||||
information.setName(name);
|
information.setName(name);
|
||||||
|
|||||||
Reference in New Issue
Block a user