Remove plugin center authentication

Squash commits of branch feature/remove_plugin_center_auth:

- Remove plugin center authentication

- Fix i18n file

- Fix tests

- Changelog entry
This commit is contained in:
Florian Scholdei
2025-06-05 09:31:55 +02:00
parent 60b672cf59
commit a2afc40432
89 changed files with 76 additions and 3135 deletions

View File

@@ -50,8 +50,7 @@
"title": {
"install": "{{name}} Plugin installieren",
"update": "{{name}} Plugin aktualisieren",
"uninstall": "{{name}} Plugin deinstallieren",
"cloudoguInstall": "{{name}} Plugin installieren"
"uninstall": "{{name}} Plugin deinstallieren"
},
"restart": "Neustarten, um Plugin-Änderungen wirksam zu machen",
"install": "Installieren",
@@ -71,8 +70,6 @@
"version": "Version",
"currentVersion": "Installierte Version",
"newVersion": "Neue Version",
"cloudoguInstallInfo": "Verbinden Sie Ihre SCM-Manager-Instanz mit ihrem cloudogu platform-Account um Zugriff auf cloudogu platform-Plugins zu erhalten. Falls Sie noch über keinen Account verfügen, können Sie sich einfach kostenlos registrieren.",
"cloudoguInstall": "Mit cloudogu platform verbinden und installieren",
"dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installiert bzw. aktualisiert, wenn sie noch nicht in der aktuellen Version vorhanden sind!",
"optionalDependencyNotification": "Mit diesem Plugin werden folgende optionale Abhängigkeiten mit aktualisiert, falls sie installiert sind!",
"dependencies": "Abhängigkeiten",
@@ -87,26 +84,6 @@
"updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam.",
"manualRestartRequired": "Nachdem die Plugin-Änderung durchgeführt wurde, muss SCM-Manager neu gestartet werden.",
"showPending": "Um die folgenden Plugin-Änderungen auszuführen, muss SCM-Manager neu gestartet werden."
},
"cloudoguPlatform": {
"connectionInfo": "Instanz ist mit der cloudogu platform verbunden.\nAccount: {{pluginCenterSubject}}",
"error": {
"info": "cloudogu platform Authentifizierungsinformationen konnten nicht abgerufen werden. Klicken Sie, um Details zu sehen.",
"title": "Fehler"
},
"failed": {
"info": "Verbindung zur cloudogu platform mit Account {{pluginCenterSubject}} is fehlgeschlagen",
"message": "Die Verbindung der SCM-Manager Instanz mit der <0>cloudogu platform</0> is fehlgeschlagen. Der Benutzer <1>{{subject}}</1> konnte nicht authentifiziert werden.",
"button": {
"label": "Erneut mit der <0>cloudogu platform</0> verbinden"
}
},
"login": {
"button": {
"label": "Mit der <0>cloudogu platform</0> verbinden"
},
"description": "Verbinden Sie Ihren SCM-Manager mit der <0>cloudogu platform</0>, um besondere Plugins zu installieren. Die cloudogu platform ist die Heimat der SCM-Manager Community, getragen von Maintainern des SCM-Managers. Sie haben noch kein Konto? Erstellen Sie während der Verbindung der SCM-Manager-Instanz kostenfrei ein cloudogu platform-Konto. <1>Mehr Details zur Datenverarbeitung.</1>"
}
}
},
"repositoryRole": {

View File

@@ -11,20 +11,6 @@
"no-write-permission-notification": "Hinweis: Es fehlen Berechtigungen zum Bearbeiten der Einstellungen!"
}
},
"pluginSettings": {
"subtitle": "Plugin Einstellungen",
"pluginUrl": "Plugin Center URL",
"pluginAuthUrl": "Plugin Center Authentifizierungs URL",
"auth": {
"loading": "Lade Authentifizierungs Informationen ...",
"notAuthenticated": "Das Plugin Center ist nicht authentifiziert",
"authenticate": "Authentifizieren",
"authenticated": "Das Plugin Center ist als <0 /> authentifiziert",
"subjectTooltip": "Authentifiziert als {{ principal }} {{ ago }}",
"logout": "Abmelden",
"notConfiguredHint": "Authentifizierungs URL ist nicht gesetzt"
}
},
"jwtSettings": {
"subtitle": "JWT Einstellungen",
"label": "Ablaufzeit",
@@ -87,6 +73,7 @@
"enabled-file-search": "Dateisuche aktivieren",
"namespace-strategy": "Namespace Strategie",
"login-info-url": "Login Info URL",
"pluginUrl": "Plugin Center URL",
"emergencyContacts": {
"label": "Notfallkontakte",
"helpText": "Liste der Benutzer, die über administrative Vorfälle informiert werden.",
@@ -116,7 +103,6 @@
"realmDescriptionHelpText": "Beschreibung des Authentication Realm.",
"dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.",
"pluginUrlHelpText": "Die URL der Plugin Center API. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur",
"pluginAuthUrlHelpText": "Die URL der Plugin Center Authentifizierungs API.",
"alertsUrlHelpText": "Die URL der Alerts API. Darüber wird über Alerts die Ihr System betreffen informiert. Um diese Funktion zu deaktivieren lassen Sie dieses Feld leer.",
"releaseFeedUrlHelpText": "Die URL des RSS Release Feed des SCM-Manager. Darüber wird über die neue SCM-Manager Version informiert. Um diese Funktion zu deaktivieren lassen Sie dieses Feld leer.",
"mailDomainNameHelpText": "Dieser Domain Name wird genutzt, wenn für einen User eine E-Mail-Adresse benötigt wird, für den keine hinterlegt ist. Diese Domain wird nicht zum Versenden von E-Mails genutzt und auch keine anderweitige Verbindung aufgebaut.",

View File

@@ -50,8 +50,7 @@
"title": {
"install": "Install {{name}} Plugin",
"update": "Update {{name}} Plugin",
"uninstall": "Uninstall {{name}} Plugin",
"cloudoguInstall": "Install {{name}} Plugin"
"uninstall": "Uninstall {{name}} Plugin"
},
"restart": "Restart to make plugin changes effective",
"install": "Install",
@@ -71,8 +70,6 @@
"version": "Version",
"currentVersion": "Installed version",
"newVersion": "New version",
"cloudoguInstallInfo": "Connect your SCM-Manager instance with your cloudogu platform account to access cloudogu platform plugins. If you do not already have an account you can easily register for free.",
"cloudoguInstall": "Connect cloudogu platform and install",
"dependencyNotification": "With this plugin, the following dependencies will be installed/updated if their latest versions are not installed yet!",
"optionalDependencyNotification": "With this plugin, the following optional dependencies will be updated if they are installed!",
"dependencies": "Dependencies",
@@ -87,26 +84,6 @@
"updateAllInfo": "The following plugin changes will be executed. You need to restart the SCM-Manager to make these changes effective.",
"manualRestartRequired": "After the plugin change has been made, SCM-Manager must be restarted.",
"showPending": "To execute the following plugin changes, SCM-Manager must be restarted."
},
"cloudoguPlatform": {
"connectionInfo": "Instance is connected to the cloudogu platform.\nAccount: {{pluginCenterSubject}}",
"error": {
"info": "Failed to retrieve cloudogu platform authentication information. Click for more details.",
"title": "Error"
},
"failed": {
"info": "Connection to the cloudogu platform failed for account {{pluginCenterSubject}}",
"message": "The connection of the SCM-Manager instance with the <0>cloudogu platform</0> failed. The user <1>{{subject}}</1> could not be authenticated. Click Reconnect to restore the connection.",
"button": {
"label": "Reconnect to the <0>cloudogu platform</0>"
}
},
"login": {
"button": {
"label": "Connect to the <0>cloudogu platform</0>"
},
"description": "Connect your SCM-Manager with the <0>cloudogu platform</0> to install special plugins. The cloudogu platform is the home of the SCM-Manager Community, sustained by the maintainers of the SCM-Manager. You don't have an account yet? Create a free cloudogu platform account while connecting your SCM-Manager instance. <1>More details on data processing.</1>"
}
}
},
"repositoryRole": {

View File

@@ -11,20 +11,6 @@
"no-write-permission-notification": "Please note: You do not have the permission to edit the config!"
}
},
"pluginSettings": {
"subtitle": "Plugin Settings",
"pluginUrl": "Plugin Center URL",
"pluginAuthUrl": "Plugin Center Authentication URL",
"auth": {
"loading": "Loading authentication info ...",
"notAuthenticated": "Plugin Center is not authenticated",
"authenticate": "Authenticate",
"authenticated": "Plugin Center is authenticated as <0 />",
"subjectTooltip": "Authenticated by {{ principal }} {{ ago }}",
"logout": "Logout",
"notConfiguredHint": "Authentication URL is not configured"
}
},
"jwtSettings": {
"subtitle": "JWT Settings",
"label": "Expiration time",
@@ -87,6 +73,7 @@
"enabled-file-search": "Enabled File Search",
"namespace-strategy": "Namespace Strategy",
"login-info-url": "Login Info URL",
"pluginUrl": "Plugin Center URL",
"emergencyContacts": {
"label": "Emergency Contacts",
"helpText": "List of users notified of administrative incidents.",
@@ -116,7 +103,6 @@
"realmDescriptionHelpText": "Enter authentication realm description.",
"dateFormatHelpText": "Moments date format. Please have a look at the MomentJS documentation.",
"pluginUrlHelpText": "The url of the Plugin Center API. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture",
"pluginAuthUrlHelpText": "The url of the Plugin Center authentication API.",
"alertsUrlHelpText": "The url of the alerts api. This provides up-to-date alerts regarding your system. To disable this feature just leave the url blank.",
"releaseFeedUrlHelpText": "The url of the RSS Release Feed for SCM-Manager. This provides up-to-date version information. To disable this feature just leave the url blank.",
"mailDomainNameHelpText": "This domain name will be used to create email addresses for users without one when needed. It will not be used to send mails nor will be accessed otherwise.",

View File

@@ -68,7 +68,6 @@ const ConfigForm: FC<Props> = ({
proxyExcludes: [],
skipFailedAuthenticators: false,
pluginUrl: "",
pluginAuthUrl: "",
loginAttemptLimitTimeout: 0,
enabledXsrfProtection: true,
enabledUserConverter: false,
@@ -149,6 +148,7 @@ const ConfigForm: FC<Props> = ({
dateFormat={innerConfig.dateFormat}
anonymousMode={innerConfig.anonymousMode}
skipFailedAuthenticators={innerConfig.skipFailedAuthenticators}
pluginUrl={innerConfig.pluginUrl}
alertsUrl={innerConfig.alertsUrl}
releaseFeedUrl={innerConfig.releaseFeedUrl}
mailDomainName={innerConfig.mailDomainName}
@@ -181,13 +181,6 @@ const ConfigForm: FC<Props> = ({
hasUpdatePermission={configUpdatePermission}
/>
<hr />
<PluginSettings
pluginUrl={innerConfig.pluginUrl}
pluginAuthUrl={innerConfig.pluginAuthUrl}
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
hasUpdatePermission={configUpdatePermission}
/>
<hr />
<JwtSettings
enabledJwtEndless={innerConfig.enabledJwtEndless || false}
jwtExpirationInH={innerConfig.jwtExpirationInH || 1}

View File

@@ -26,6 +26,7 @@ import classNames from "classnames";
type Props = {
realmDescription: string;
loginInfoUrl: string;
pluginUrl: string;
disableGroupingGrid: boolean;
dateFormat: string;
anonymousMode: AnonymousMode;
@@ -44,6 +45,7 @@ type Props = {
const GeneralSettings: FC<Props> = ({
realmDescription,
loginInfoUrl,
pluginUrl,
anonymousMode,
alertsUrl,
releaseFeedUrl,
@@ -62,6 +64,9 @@ const GeneralSettings: FC<Props> = ({
const handleLoginInfoUrlChange = (value: string) => {
onChange(true, value, "loginInfoUrl");
};
const handlePluginCenterUrlChange = (value: string) => {
onChange(true, value, "pluginUrl");
};
const handleRealmDescriptionChange = (value: string) => {
onChange(true, value, "realmDescription");
};
@@ -127,6 +132,17 @@ const GeneralSettings: FC<Props> = ({
/>
</div>
</div>
<div className="columns">
<div className="column">
<InputField
label={t("general-settings.pluginUrl")}
onChange={handlePluginCenterUrlChange}
value={pluginUrl}
disabled={!hasUpdatePermission}
helpText={t("help.pluginUrlHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column">
<Checkbox
@@ -148,9 +164,9 @@ const GeneralSettings: FC<Props> = ({
disabled={!hasUpdatePermission}
className="is-fullwidth"
options={[
{ label: t("general-settings.anonymousMode.full"), value: "FULL" },
{ label: t("general-settings.anonymousMode.protocolOnly"), value: "PROTOCOL_ONLY" },
{ label: t("general-settings.anonymousMode.off"), value: "OFF" },
{label: t("general-settings.anonymousMode.full"), value: "FULL"},
{label: t("general-settings.anonymousMode.protocolOnly"), value: "PROTOCOL_ONLY"},
{label: t("general-settings.anonymousMode.off"), value: "OFF"},
]}
helpText={t("help.allowAnonymousAccessHelpText")}
testId={"anonymous-mode-select"}
@@ -197,12 +213,12 @@ const GeneralSettings: FC<Props> = ({
helpText={t("general-settings.emergencyContacts.helpText")}
placeholder={t("general-settings.emergencyContacts.autocompletePlaceholder")}
aria-label="general-settings.emergencyContacts.ariaLabel"
value={emergencyContacts.map((m) => ({ label: m, value: { id: m, displayName: m } }))}
value={emergencyContacts.map((m) => ({label: m, value: {id: m, displayName: m}}))}
onChange={handleEmergencyContactsChange}
>
<Combobox<AutocompleteObject>
options={userOptions || []}
className={classNames({ "is-loading": userOptionsLoading })}
className={classNames({"is-loading": userOptionsLoading})}
onQueryChange={setQuery}
/>
</ChipInputField>

View File

@@ -1,123 +0,0 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
import React, { FC } from "react";
import { usePluginCenterAuthInfo, usePluginCenterLogout } from "@scm-manager/ui-api";
import { Button, ErrorNotification, Notification, Tooltip, useDateFormatter } from "@scm-manager/ui-components";
import { Link, PluginCenterAuthenticationInfo } from "@scm-manager/ui-types";
import styled from "styled-components";
import { Trans, useTranslation } from "react-i18next";
const Message = styled.p`
line-height: 2.5rem;
`;
type Props = {
authenticationInfo: PluginCenterAuthenticationInfo;
};
const PluginCenterSubject: FC<Props> = ({ authenticationInfo }) => {
const formatter = useDateFormatter({ date: authenticationInfo.date });
const [t] = useTranslation("config");
return (
<>
<Tooltip
location="top"
message={t("pluginSettings.auth.subjectTooltip", {
principal: authenticationInfo.principal,
ago: formatter?.formatDistance()
})}
>
<strong>{authenticationInfo.pluginCenterSubject}</strong>
</Tooltip>
</>
);
};
const AuthenticatedInfo: FC<Props> = ({ authenticationInfo }) => {
const { logout, isLoading, error } = usePluginCenterLogout(authenticationInfo);
const [t] = useTranslation("config");
const subject = <PluginCenterSubject authenticationInfo={authenticationInfo} />;
return (
<Notification type="inherit">
<div className="is-full-width is-flex is-justify-content-space-between is-align-content-center">
<Message>
<Trans t={t} i18nKey="pluginSettings.auth.authenticated" components={[subject]} />
</Message>
{authenticationInfo._links.logout ? (
<Button color="warning" loading={isLoading} action={logout}>
{t("pluginSettings.auth.logout")}
</Button>
) : null}
</div>
{error ? (
<div className="pt-4">
<ErrorNotification error={error} />
</div>
) : null}
</Notification>
);
};
const LoginButton: FC<{ link: Link }> = ({ link }) => {
const [t] = useTranslation("config");
return (
<Button color="primary" link={link.href}>
{t("pluginSettings.auth.authenticate")}
</Button>
);
};
const PluginCenterAuthentication: FC = () => {
const { data, isLoading, error } = usePluginCenterAuthInfo();
const [t] = useTranslation("config");
if (isLoading) {
return (
<div className="is-flex is-align-content-center">
<span className="small-loading-spinner pt-1 pr-3" />
<p>{t("pluginSettings.auth.loading")}</p>
</div>
);
}
if (error) {
return <ErrorNotification error={error} />;
}
if (!data) {
return null;
}
if (data.principal) {
return <AuthenticatedInfo authenticationInfo={data} />;
}
if (data._links.login) {
return (
<Notification type="inherit" className="is-flex is-justify-content-space-between is-align-content-center">
<Message>{t("pluginSettings.auth.notAuthenticated")}</Message>
<LoginButton link={data._links.login as Link} />
</Notification>
);
} else {
return null;
}
};
export default PluginCenterAuthentication;

View File

@@ -1,70 +0,0 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { InputField, Subtitle } from "@scm-manager/ui-components";
import PluginCenterAuthentication from "./PluginCenterAuthentication";
type Props = {
pluginUrl: string;
pluginAuthUrl: string;
onChange: (isValid: boolean, changedValue: string, name: string) => void;
hasUpdatePermission: boolean;
};
const PluginSettings: FC<Props> = ({ pluginUrl, pluginAuthUrl, onChange, hasUpdatePermission }) => {
const { t } = useTranslation("config");
const handlePluginCenterUrlChange = (value: string) => {
onChange(true, value, "pluginUrl");
};
const handlePluginCenterAuthUrlChange = (value: string) => {
onChange(true, value, "pluginAuthUrl");
};
return (
<div>
<Subtitle subtitle={t("pluginSettings.subtitle")} />
<div className="columns">
<div className="column">
<InputField
label={t("pluginSettings.pluginUrl")}
onChange={handlePluginCenterUrlChange}
value={pluginUrl}
disabled={!hasUpdatePermission}
helpText={t("help.pluginUrlHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column">
<InputField
label={t("pluginSettings.pluginAuthUrl")}
onChange={handlePluginCenterAuthUrlChange}
value={pluginAuthUrl}
disabled={!hasUpdatePermission}
helpText={t("help.pluginAuthUrlHelpText")}
/>
</div>
</div>
<PluginCenterAuthentication />
</div>
);
};
export default PluginSettings;

View File

@@ -1,117 +0,0 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
import React, { FC } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Link, PluginCenterAuthenticationInfo } from "@scm-manager/ui-types";
import classNames from "classnames";
import styled from "styled-components";
import { ExternalLinkButton, Icon } from "@scm-manager/ui-core";
type Props = {
info: PluginCenterAuthenticationInfo;
};
const CloudoguPlatformBanner: FC<Props> = ({ info }) => {
const loginLink = (info._links.login as Link)?.href;
if (loginLink) {
return <Unauthenticated info={info} link={loginLink} />;
}
if (info.failed) {
const reconnectLink = (info._links.reconnect as Link)?.href;
if (reconnectLink) {
return <FailedAuthentication info={info} link={reconnectLink} />;
}
}
return null;
};
type PropsWithLink = Props & {
link: string;
};
const FailedAuthentication: FC<PropsWithLink> = ({ info, link }) => {
const [t] = useTranslation("admin");
return (
<Container className="has-border-danger">
<p className="is-align-self-flex-start">
<Trans
t={t}
i18nKey="plugins.cloudoguPlatform.failed.message"
values={{ subject: info.pluginCenterSubject }}
components={[<a href="https://platform.cloudogu.com/">cloudogu platform</a>, <strong />]}
/>
</p>
<ExternalLinkButton className="mt-5 has-text-weight-normal has-border-info" href={link}>
<Trans
t={t}
i18nKey="plugins.cloudoguPlatform.failed.button.label"
components={[<span className="mx-1 has-text-info">cloudogu platform</span>]}
/>
</ExternalLinkButton>
</Container>
);
};
type ContainerProps = {
className?: string;
};
const Container: FC<ContainerProps> = ({ className, children }) => (
<DivWithSolidBorder
className={classNames(
"has-rounded-border is-flex is-flex-direction-column is-align-items-center p-5 mb-4",
className
)}
>
{children}
</DivWithSolidBorder>
);
const DivWithSolidBorder = styled.div`
border: 2px solid;
`;
const Unauthenticated: FC<PropsWithLink> = ({ link, info }) => {
const [t] = useTranslation("admin");
return (
<Container className="has-border-success">
<ExternalLinkButton className="mb-5 has-text-weight-normal has-border-info" href={link}>
<Trans
t={t}
i18nKey="plugins.cloudoguPlatform.login.button.label"
components={[<span className="mx-1 has-text-info">cloudogu platform</span>]}
/>
</ExternalLinkButton>
<p className="is-align-self-flex-start is-size-7">
<span>
<Trans
t={t}
i18nKey="plugins.cloudoguPlatform.login.description"
components={[
<a href="https://platform.cloudogu.com/">cloudogu platform</a>,
<a href="https://scm-manager.org/data-processing">Data Processing</a>,
]}
/>
</span>
</p>
</Container>
);
};
export default CloudoguPlatformBanner;

View File

@@ -1,30 +0,0 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
import React from "react";
import styled from "styled-components";
const CloudoguPlatformTagWrapper = styled.span`
border: solid 1px;
`;
const CloudoguPlatformTag = () => (
<CloudoguPlatformTagWrapper className="has-text-info has-border-info has-rounded-border p-1 is-size-7">
cloudogu platform
</CloudoguPlatformTagWrapper>
);
export default CloudoguPlatformTag;

View File

@@ -16,19 +16,16 @@
import React, { FC } from "react";
import styled from "styled-components";
import { Link, Plugin, PluginCenterAuthenticationInfo } from "@scm-manager/ui-types";
import { Link, Plugin } from "@scm-manager/ui-types";
import { CardColumn, Icon } from "@scm-manager/ui-components";
import { PluginAction, PluginModalContent } from "../containers/PluginsOverview";
import { useTranslation } from "react-i18next";
import PluginAvatar from "./PluginAvatar";
import classNames from "classnames";
import CloudoguPlatformTag from "./CloudoguPlatformTag";
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
type Props = {
plugin: Plugin;
openModal: (content: PluginModalContent) => void;
pluginCenterAuthInfo?: PluginCenterAuthenticationInfo;
};
const ActionbarWrapper = styled.div`
@@ -37,7 +34,7 @@ const ActionbarWrapper = styled.div`
}
`;
const IconWrapperStyle = styled.span.attrs((props) => ({
const IconWrapperStyle = styled.span.attrs(() => ({
className: "level-item mb-0 p-2 is-clickable",
}))`
border: 1px solid #cdcdcd; // $dark-25
@@ -56,13 +53,11 @@ const IconWrapper: FC<{ action: () => void }> = ({ action, children }) => {
);
};
const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) => {
const PluginEntry: FC<Props> = ({ plugin, openModal }) => {
const [t] = useTranslation("admin");
const isInstallable = plugin._links.install && (plugin._links.install as Link).href;
const isUpdatable = plugin._links.update && (plugin._links.update as Link).href;
const isUninstallable = plugin._links.uninstall && (plugin._links.uninstall as Link).href;
const isCloudoguPlugin = plugin.type === "CLOUDOGU";
const isDefaultPluginCenterLoginAvailable = pluginCenterAuthInfo?.default && !!pluginCenterAuthInfo?._links?.login;
const ref = useKeyboardIteratorTarget();
const evaluateAction = () => {
@@ -70,29 +65,16 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
return () => openModal({ plugin, action: PluginAction.INSTALL });
}
if (isCloudoguPlugin && isDefaultPluginCenterLoginAvailable) {
return () => openModal({ plugin, action: PluginAction.CLOUDOGU });
}
return undefined;
};
const pendingInfo = () => (
<>
<Icon
className="fa-lg"
name="check"
color="info"
alt={t("plugins.markedAsPending")}
/></>
<Icon className="fa-lg" name="check" color="info" alt={t("plugins.markedAsPending")} />
</>
);
const actionBar = () => (
<ActionbarWrapper className="is-flex">
{isCloudoguPlugin && isDefaultPluginCenterLoginAvailable && (
<IconWrapper action={() => openModal({ plugin, action: PluginAction.CLOUDOGU })}>
<Icon title={t("plugins.modal.cloudoguInstall")} name="link" color="success-dark" />
</IconWrapper>
)}
{isInstallable && (
<IconWrapper action={() => openModal({ plugin, action: PluginAction.INSTALL })}>
<Icon title={t("plugins.modal.install")} name="download" color="info" />
@@ -121,17 +103,8 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
description={plugin.description}
contentRight={plugin.pending || plugin.markedForUninstall ? pendingInfo() : actionBar()}
footerLeft={<small>{plugin.version}</small>}
footerRight={null}
footerRight={<small className="level-item is-block shorten-text">{plugin.author}</small>}
/>
<div
className={classNames("is-flex", {
"is-justify-content-space-between": isCloudoguPlugin,
"is-justify-content-end": !isCloudoguPlugin,
})}
>
{isCloudoguPlugin ? <CloudoguPlatformTag /> : null}
<small className="level-item is-block shorten-text is-align-self-flex-end">{plugin.author}</small>
</div>
</>
);
};

View File

@@ -16,26 +16,18 @@
import React, { FC } from "react";
import { CardColumnGroup } from "@scm-manager/ui-components";
import { PluginCenterAuthenticationInfo, PluginGroup } from "@scm-manager/ui-types";
import { PluginGroup } from "@scm-manager/ui-types";
import PluginEntry from "./PluginEntry";
import { PluginModalContent } from "../containers/PluginsOverview";
type Props = {
group: PluginGroup;
openModal: (content: PluginModalContent) => void;
pluginCenterAuthInfo?: PluginCenterAuthenticationInfo;
};
const PluginGroupEntry: FC<Props> = ({ openModal, group, pluginCenterAuthInfo }) => {
const entries = group.plugins.map(plugin => {
return (
<PluginEntry
plugin={plugin}
openModal={openModal}
key={plugin.name}
pluginCenterAuthInfo={pluginCenterAuthInfo}
/>
);
const PluginGroupEntry: FC<Props> = ({ openModal, group }) => {
const entries = group.plugins.map((plugin) => {
return <PluginEntry plugin={plugin} openModal={openModal} key={plugin.name} />;
});
return <CardColumnGroup name={group.name} elements={entries} />;
};

View File

@@ -15,7 +15,7 @@
*/
import React, { FC } from "react";
import { Plugin, PluginCenterAuthenticationInfo } from "@scm-manager/ui-types";
import { Plugin } from "@scm-manager/ui-types";
import PluginGroupEntry from "../components/PluginGroupEntry";
import groupByCategory from "./groupByCategory";
import { PluginModalContent } from "../containers/PluginsOverview";
@@ -24,23 +24,15 @@ import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
type Props = {
plugins: Plugin[];
openModal: (content: PluginModalContent) => void;
pluginCenterAuthInfo?: PluginCenterAuthenticationInfo;
};
const PluginList: FC<Props> = ({ plugins, openModal, pluginCenterAuthInfo }) => {
const PluginList: FC<Props> = ({ plugins, openModal }) => {
const groups = groupByCategory(plugins);
return (
<div className="content is-plugin-page">
<KeyboardIterator>
{groups.map((group) => {
return (
<PluginGroupEntry
group={group}
openModal={openModal}
key={group.name}
pluginCenterAuthInfo={pluginCenterAuthInfo}
/>
);
return <PluginGroupEntry group={group} openModal={openModal} key={group.name} />;
})}
</KeyboardIterator>
</div>

View File

@@ -18,12 +18,11 @@ import React, { FC, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import styled from "styled-components";
import { Link, Plugin } from "@scm-manager/ui-types";
import { Plugin } from "@scm-manager/ui-types";
import { Button, ButtonGroup, Checkbox, ErrorNotification, Modal, Notification } from "@scm-manager/ui-components";
import SuccessNotification from "./SuccessNotification";
import { useInstallPlugin, usePluginCenterAuthInfo, useUninstallPlugin, useUpdatePlugins } from "@scm-manager/ui-api";
import { useInstallPlugin, useUninstallPlugin, useUpdatePlugins } from "@scm-manager/ui-api";
import { PluginAction } from "../containers/PluginsOverview";
import CloudoguPlatformTag from "./CloudoguPlatformTag";
type Props = {
plugin: Plugin;
@@ -48,16 +47,11 @@ const ListChild = styled.div`
const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
const [t] = useTranslation("admin");
const [shouldRestart, setShouldRestart] = useState<boolean>(false);
const {
data: pluginCenterAuthInfo,
isLoading: isLoadingPluginCenterAuthInfo,
error: pluginCenterAuthInfoError
} = usePluginCenterAuthInfo();
const { isLoading: isInstalling, error: installError, install, isInstalled } = useInstallPlugin();
const { isLoading: isUninstalling, error: uninstallError, uninstall, isUninstalled } = useUninstallPlugin();
const { isLoading: isUpdating, error: updateError, update, isUpdated } = useUpdatePlugins();
const error = installError || uninstallError || updateError || pluginCenterAuthInfoError;
const loading = isInstalling || isUninstalling || isUpdating || isLoadingPluginCenterAuthInfo;
const error = installError || uninstallError || updateError;
const loading = isInstalling || isUninstalling || isUpdating;
const isDone = isInstalled || isUninstalled || isUpdated;
const initialFocusRef = useRef<HTMLButtonElement>(null);
@@ -70,9 +64,6 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
const handlePluginAction = (e: React.MouseEvent<Element, MouseEvent>) => {
e.preventDefault();
switch (pluginAction) {
case PluginAction.CLOUDOGU:
window.open((pluginCenterAuthInfo?._links?.login as Link).href, "_self");
break;
case PluginAction.INSTALL:
install(plugin, { restart: shouldRestart });
break;
@@ -195,7 +186,7 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
disabled={false}
/>
);
} else if (pluginAction !== PluginAction.CLOUDOGU) {
} else {
return <Notification type="warning">{t("plugins.modal.manualRestartRequired")}</Notification>;
}
};
@@ -213,18 +204,6 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
<ListParent pluginAction={pluginAction}>{t("plugins.modal.author")}:</ListParent>
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.author}</ListChild>
</div>
{pluginAction === PluginAction.CLOUDOGU && (
<>
<div className="field is-horizontal">
<CloudoguPlatformTag />
</div>
<div className="field is-horizontal">
<Notification type="info" className="is-full-width">
{t("plugins.modal.cloudoguInstallInfo")}
</Notification>
</div>
</>
)}
{pluginAction === PluginAction.INSTALL && (
<div className="field is-horizontal">
<ListParent pluginAction={pluginAction}>{t("plugins.modal.version")}:</ListParent>

View File

@@ -29,11 +29,8 @@ import {
useAvailablePlugins,
useInstalledPlugins,
usePendingPlugins,
usePluginCenterAuthInfo,
} from "@scm-manager/ui-api";
import PluginModal from "../components/PluginModal";
import CloudoguPlatformBanner from "../components/CloudoguPlatformBanner";
import PluginCenterAuthInfo from "../components/PluginCenterAuthInfo";
import styled from "styled-components";
import { Button } from "@scm-manager/ui-buttons";
import { useDocumentTitle } from "@scm-manager/ui-core";
@@ -42,7 +39,6 @@ export enum PluginAction {
INSTALL = "install",
UPDATE = "update",
UNINSTALL = "uninstall",
CLOUDOGU = "cloudoguInstall",
}
export type PluginModalContent = {
@@ -83,7 +79,6 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
error: installedPluginsError,
} = useInstalledPlugins({ enabled: installed });
const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins();
const pluginCenterAuthInfo = usePluginCenterAuthInfo();
const [showPendingModal, setShowPendingModal] = useState(false);
const [showExecutePendingModal, setShowExecutePendingModal] = useState(false);
const [showUpdateAllModal, setShowUpdateAllModal] = useState(false);
@@ -97,9 +92,7 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
return (
<StickyHeader className="has-background-secondary-least is-flex is-justify-content-space-between is-align-items-baseline has-gap-2">
<div className="is-flex-shrink-0">
<Title className="is-flex">
{t("plugins.title")} <PluginCenterAuthInfo {...pluginCenterAuthInfo} />
</Title>
<Title>{t("plugins.title")}</Title>
<Subtitle subtitle={installed ? t("plugins.installedSubtitle") : t("plugins.availableSubtitle")} />
</div>
<PluginTopActions>{actions}</PluginTopActions>
@@ -166,11 +159,7 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
return (
<>
{pluginCenterStatusNotification}
<PluginsList
plugins={collection._embedded.plugins}
openModal={setPluginModalContent}
pluginCenterAuthInfo={pluginCenterAuthInfo.data}
/>
<PluginsList plugins={collection._embedded.plugins} openModal={setPluginModalContent} />
</>
);
}
@@ -211,7 +200,6 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
return (
<>
{renderHeader(actions)}
{pluginCenterAuthInfo.data?.default ? <CloudoguPlatformBanner info={pluginCenterAuthInfo.data} /> : null}
{renderPluginsList()}
{renderModals()}
</>