mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-08 15:46:54 +01:00
Refactor plugin manager
Make the plugin manager functions more clear and improve the usability by using a sticky top area with action buttons. Committed-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
2
gradle/plugin-center.yaml
Normal file
2
gradle/plugin-center.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: changed
|
||||
description: Refactor plugin manager
|
||||
@@ -38,11 +38,11 @@
|
||||
},
|
||||
"markedAsPending": "Als ausstehend markiert",
|
||||
"showPending": "Änderungen anzeigen",
|
||||
"executePending": "Änderungen ausführen",
|
||||
"executePending": "Warten auf Neustart",
|
||||
"outdatedPlugins": "{{count}} Plugin aktualisieren",
|
||||
"outdatedPlugins_plural": "{{count}} Plugins aktualisieren",
|
||||
"updateAll": "Alle Plugins aktualisieren",
|
||||
"cancelPending": "Änderungen abbrechen",
|
||||
"cancelPending": "Änderungen verwerfen",
|
||||
"noPlugins": "Keine Plugins gefunden.",
|
||||
"pluginCenterStatus": {
|
||||
"ERROR": "Das Plugin Center ist nicht verfügbar. Plugins können weder installiert noch aktualisiert werden.",
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
},
|
||||
"markedAsPending": "Marked as pending",
|
||||
"showPending": "Show Changes",
|
||||
"executePending": "Execute Changes",
|
||||
"executePending": "Waiting for restart",
|
||||
"outdatedPlugins": "Update {{count}} Plugin",
|
||||
"outdatedPlugins_plural": "Update {{count}} Plugins",
|
||||
"updateAll": "Update All Plugins",
|
||||
"cancelPending": "Cancel Changes",
|
||||
"cancelPending": "Discard Changes",
|
||||
"noPlugins": "No plugins found.",
|
||||
"pluginCenterStatus": {
|
||||
"ERROR": "The Plugin Center is not available. Plugins can neither be installed nor updated.",
|
||||
|
||||
@@ -25,8 +25,9 @@ import * as React from "react";
|
||||
import { FC, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
|
||||
import { Button, ButtonGroup, ErrorNotification, Modal } from "@scm-manager/ui-components";
|
||||
import { ButtonGroup, ErrorNotification, Modal } from "@scm-manager/ui-components";
|
||||
import SuccessNotification from "./SuccessNotification";
|
||||
import {Button} from "@scm-manager/ui-buttons";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
@@ -143,22 +144,20 @@ const PluginActionModal: FC<Props> = ({
|
||||
<ButtonGroup>
|
||||
{success ? (
|
||||
<Button
|
||||
label={t("plugins.modal.reload")}
|
||||
action={() => window.location.reload()}
|
||||
onClick={() => window.location.reload()}
|
||||
variant="primary"
|
||||
color="success"
|
||||
icon="sync-alt"
|
||||
/>
|
||||
>{t("plugins.modal.reload")}</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
color="warning"
|
||||
label={label}
|
||||
loading={loading}
|
||||
action={execute}
|
||||
variant="primary"
|
||||
isLoading={loading}
|
||||
onClick={execute}
|
||||
disabled={!!error || success}
|
||||
ref={initialFocusRef}
|
||||
/>
|
||||
<Button label={t("plugins.modal.abort")} action={onClose} />
|
||||
>{label}</Button>
|
||||
<Button onClick={onClose} >{t("plugins.modal.abort")}</Button>
|
||||
</>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
children?: React.Node;
|
||||
};
|
||||
|
||||
const ActionWrapper = styled.div`
|
||||
border: 2px solid var(--scm-border-color);
|
||||
`;
|
||||
|
||||
export default class PluginBottomActions extends React.Component<Props> {
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
return (
|
||||
<ActionWrapper className={classNames("is-flex", "is-justify-content-center", "mt-5", "p-4")}>
|
||||
{children}
|
||||
</ActionWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,13 +84,14 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const pendingSpinner = () => (
|
||||
<Icon
|
||||
className="fa-spin fa-lg"
|
||||
name="spinner"
|
||||
color={plugin.markedForUninstall ? "danger" : "info"}
|
||||
const pendingInfo = () => (
|
||||
<>
|
||||
<Icon
|
||||
className="fa-lg"
|
||||
name="check"
|
||||
color="info"
|
||||
alt={t("plugins.markedAsPending")}
|
||||
/>
|
||||
/></>
|
||||
);
|
||||
const actionBar = () => (
|
||||
<ActionbarWrapper className="is-flex">
|
||||
@@ -125,7 +126,7 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
|
||||
avatar={<PluginAvatar plugin={plugin} />}
|
||||
title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>}
|
||||
description={plugin.description}
|
||||
contentRight={plugin.pending || plugin.markedForUninstall ? pendingSpinner() : actionBar()}
|
||||
contentRight={plugin.pending || plugin.markedForUninstall ? pendingInfo() : actionBar()}
|
||||
footerLeft={<small>{plugin.version}</small>}
|
||||
footerRight={null}
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,7 @@ import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
children?: React.Node;
|
||||
children?: React.ReactElement;
|
||||
};
|
||||
|
||||
export default class PluginTopActions extends React.Component<Props> {
|
||||
@@ -34,9 +34,6 @@ export default class PluginTopActions extends React.Component<Props> {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"column",
|
||||
"is-one-fifths",
|
||||
"is-mobile-action-spacing",
|
||||
"is-flex",
|
||||
"is-justify-content-flex-end",
|
||||
"is-align-items-center"
|
||||
|
||||
@@ -22,10 +22,11 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { Button, Modal, Notification } from "@scm-manager/ui-components";
|
||||
import { Modal, Notification } from "@scm-manager/ui-components";
|
||||
import { PendingPlugins } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PendingPluginsQueue from "./PendingPluginsQueue";
|
||||
import { Button } from "@scm-manager/ui-buttons";
|
||||
|
||||
type ModalBodyProps = {
|
||||
pendingPlugins: PendingPlugins;
|
||||
@@ -60,7 +61,7 @@ const ShowPendingModal: FC<Props> = ({ pendingPlugins, onClose }) => {
|
||||
title={t("plugins.showPending")}
|
||||
closeFunction={onClose}
|
||||
body={<ModalBody pendingPlugins={pendingPlugins} />}
|
||||
footer={<Button label={t("plugins.modal.close")} action={onClose} />}
|
||||
footer={<Button onClick={onClose}>{t("plugins.modal.close")}</Button>}
|
||||
active={true}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -25,18 +25,9 @@ import * as React from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plugin } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ErrorNotification,
|
||||
Loading,
|
||||
Notification,
|
||||
Subtitle,
|
||||
Title,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { ButtonGroup, ErrorNotification, Loading, Notification, Subtitle, Title } from "@scm-manager/ui-components";
|
||||
import PluginsList from "../components/PluginList";
|
||||
import PluginTopActions from "../components/PluginTopActions";
|
||||
import PluginBottomActions from "../components/PluginBottomActions";
|
||||
import ExecutePendingActionModal from "../components/ExecutePendingActionModal";
|
||||
import CancelPendingActionModal from "../components/CancelPendingActionModal";
|
||||
import UpdateAllActionModal from "../components/UpdateAllActionModal";
|
||||
@@ -50,6 +41,8 @@ import {
|
||||
import PluginModal from "../components/PluginModal";
|
||||
import MyCloudoguBanner from "../components/MyCloudoguBanner";
|
||||
import PluginCenterAuthInfo from "../components/PluginCenterAuthInfo";
|
||||
import styled from "styled-components";
|
||||
import { Button } from "@scm-manager/ui-buttons";
|
||||
|
||||
export enum PluginAction {
|
||||
INSTALL = "install",
|
||||
@@ -67,6 +60,17 @@ type Props = {
|
||||
installed: boolean;
|
||||
};
|
||||
|
||||
const StickyHeader = styled.div`
|
||||
position: sticky;
|
||||
top: 52px;
|
||||
z-index: 10;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: -1rem;
|
||||
border-bottom: solid 2px var(--scm-border-color);
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
`;
|
||||
|
||||
const PluginsOverview: FC<Props> = ({ installed }) => {
|
||||
const [t] = useTranslation("admin");
|
||||
const {
|
||||
@@ -90,88 +94,60 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
|
||||
const error = (installed ? installedPluginsError : availablePluginsError) || pendingPluginsError;
|
||||
const loading = (installed ? isLoadingInstalledPlugins : isLoadingAvailablePlugins) || isLoadingPendingPlugins;
|
||||
|
||||
const renderHeader = (actions: React.ReactNode) => {
|
||||
const renderHeader = (actions: React.ReactElement) => {
|
||||
return (
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<Title className="is-flex">
|
||||
{t("plugins.title")} <PluginCenterAuthInfo {...pluginCenterAuthInfo} />
|
||||
</Title>
|
||||
<Subtitle subtitle={installed ? t("plugins.installedSubtitle") : t("plugins.availableSubtitle")} />
|
||||
<StickyHeader className="has-background-secondary-least ">
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-baseline">
|
||||
<div>
|
||||
<Title>
|
||||
{t("plugins.title")} <PluginCenterAuthInfo {...pluginCenterAuthInfo} />
|
||||
</Title>
|
||||
<Subtitle subtitle={installed ? t("plugins.installedSubtitle") : t("plugins.availableSubtitle")} />
|
||||
</div>
|
||||
<PluginTopActions>{actions}</PluginTopActions>
|
||||
</div>
|
||||
<PluginTopActions>{actions}</PluginTopActions>
|
||||
</div>
|
||||
</StickyHeader>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFooter = (actions: React.ReactNode) => {
|
||||
if (actions) {
|
||||
return <PluginBottomActions>{actions}</PluginBottomActions>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const createActions = () => {
|
||||
const buttons = [];
|
||||
|
||||
if (pendingPlugins && pendingPlugins._links) {
|
||||
if (pendingPlugins._links.execute) {
|
||||
buttons.push(
|
||||
<Button
|
||||
color="primary"
|
||||
reducedMobile={true}
|
||||
key={"executePending"}
|
||||
icon={"arrow-circle-right"}
|
||||
label={t("plugins.executePending")}
|
||||
action={() => setShowExecutePendingModal(true)}
|
||||
/>
|
||||
<Button variant="primary" key={"executePending"} onClick={() => setShowExecutePendingModal(true)}>
|
||||
{t("plugins.executePending")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (pendingPlugins._links.cancel) {
|
||||
if (!pendingPlugins._links.execute) {
|
||||
buttons.push(
|
||||
<Button
|
||||
color="primary"
|
||||
reducedMobile={true}
|
||||
key={"showPending"}
|
||||
icon={"info"}
|
||||
label={t("plugins.showPending")}
|
||||
action={() => setShowPendingModal(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (pendingPlugins._links.cancel && !pendingPlugins._links.execute) {
|
||||
buttons.push(
|
||||
<Button
|
||||
color="primary"
|
||||
reducedMobile={true}
|
||||
key={"cancelPending"}
|
||||
icon={"times"}
|
||||
label={t("plugins.cancelPending")}
|
||||
action={() => setShowCancelModal(true)}
|
||||
/>
|
||||
<Button variant="primary" key={"showPending"} onClick={() => setShowPendingModal(true)}>
|
||||
{t("plugins.showPending")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (collection && collection._links && collection._links.update) {
|
||||
buttons.push(
|
||||
<Button
|
||||
color="primary"
|
||||
reducedMobile={true}
|
||||
key={"updateAll"}
|
||||
icon={"sync-alt"}
|
||||
label={computeUpdateAllSize()}
|
||||
action={() => setShowUpdateAllModal(true)}
|
||||
/>
|
||||
<Button variant="secondary" key={"updateAll"} onClick={() => setShowUpdateAllModal(true)}>
|
||||
{computeUpdateAllSize()}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (buttons.length > 0) {
|
||||
return <ButtonGroup>{buttons}</ButtonGroup>;
|
||||
if (pendingPlugins && pendingPlugins._links && pendingPlugins._links.cancel) {
|
||||
buttons.push(
|
||||
<Button key={"cancelPending"} onClick={() => setShowCancelModal(true)}>
|
||||
{t("plugins.cancelPending")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
return <>{buttons.length > 0 ? <ButtonGroup>{buttons}</ButtonGroup> : null}</>;
|
||||
};
|
||||
|
||||
const computeUpdateAllSize = () => {
|
||||
@@ -238,10 +214,8 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
|
||||
return (
|
||||
<>
|
||||
{renderHeader(actions)}
|
||||
<hr className="header-with-actions" />
|
||||
{pluginCenterAuthInfo.data?.default ? <MyCloudoguBanner info={pluginCenterAuthInfo.data} /> : null}
|
||||
{renderPluginsList()}
|
||||
{renderFooter(actions)}
|
||||
{renderModals()}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user