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:
Eduard Heimbuch
2023-04-18 10:06:40 +02:00
parent 0ded2ce352
commit 40c4e1672c
9 changed files with 71 additions and 142 deletions

View File

@@ -0,0 +1,2 @@
- type: changed
description: Refactor plugin manager

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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>

View File

@@ -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>
);
}
}

View File

@@ -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}
/>

View File

@@ -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"

View File

@@ -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}
/>
);

View File

@@ -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()}
</>
);