diff --git a/gradle/changelog/pc_auth_failure.yml b/gradle/changelog/pc_auth_failure.yml new file mode 100644 index 0000000000..65c046c0dd --- /dev/null +++ b/gradle/changelog/pc_auth_failure.yml @@ -0,0 +1,2 @@ +- type: changed + description: Fetch plugins without authentication, if prior authentication failed ([#1940](https://github.com/scm-manager/scm-manager/pull/1940)) diff --git a/scm-ui/ui-api/src/usePluginCenterAuthInfo.ts b/scm-ui/ui-api/src/usePluginCenterAuthInfo.ts index a160625a20..8f64d4ba5d 100644 --- a/scm-ui/ui-api/src/usePluginCenterAuthInfo.ts +++ b/scm-ui/ui-api/src/usePluginCenterAuthInfo.ts @@ -28,6 +28,16 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; import { apiClient } from "./apiclient"; import { useLocation } from "react-router-dom"; +const appendQueryParam = (link: Link, name: string, value: string) => { + let href = link.href; + if (href.includes("?")) { + href += "&"; + } else { + href += "?"; + } + link.href = href + name + "=" + value; +}; + export const usePluginCenterAuthInfo = (): ApiResult => { const link = useIndexLink("pluginCenterAuth"); const location = useLocation(); @@ -42,7 +52,10 @@ export const usePluginCenterAuthInfo = (): ApiResult response.json()) .then((result: PluginCenterAuthenticationInfo) => { if (result._links?.login) { - (result._links.login as Link).href += `?source=${location.pathname}`; + appendQueryParam(result._links.login as Link, "source", location.pathname); + } + if (result._links?.reconnect) { + appendQueryParam(result._links.reconnect as Link, "source", location.pathname); } return result; }); diff --git a/scm-ui/ui-components/src/SmallLoadingSpinner.tsx b/scm-ui/ui-components/src/SmallLoadingSpinner.tsx index 85eae8ec72..1342330f1f 100644 --- a/scm-ui/ui-components/src/SmallLoadingSpinner.tsx +++ b/scm-ui/ui-components/src/SmallLoadingSpinner.tsx @@ -22,13 +22,16 @@ * SOFTWARE. */ import React, { FC } from "react"; +import classNames from "classnames"; -const SmallLoadingSpinner: FC = () => { - return ( -
-
-
- ); +type Props = { + className?: string; }; +const SmallLoadingSpinner: FC = ({ className }) => ( +
+
+
+); + export default SmallLoadingSpinner; diff --git a/scm-ui/ui-styles/src/components/_main.scss b/scm-ui/ui-styles/src/components/_main.scss index 1463512240..375264aecd 100644 --- a/scm-ui/ui-styles/src/components/_main.scss +++ b/scm-ui/ui-styles/src/components/_main.scss @@ -370,6 +370,18 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; border-color: $info !important; } +.has-border-danger { + border-color: $danger !important; +} + +.has-border-warning { + border-color: $warning !important; +} + +.has-border-primary { + border-color: $danger !important; +} + ul.is-separated { > li:after { content: ",\2800"; diff --git a/scm-ui/ui-types/src/Plugin.ts b/scm-ui/ui-types/src/Plugin.ts index c351aea7d8..f4b657c1a2 100644 --- a/scm-ui/ui-types/src/Plugin.ts +++ b/scm-ui/ui-types/src/Plugin.ts @@ -65,4 +65,5 @@ export type PluginCenterAuthenticationInfo = HalRepresentation & { pluginCenterSubject?: string; date?: string; default: boolean; + failed: boolean; }; diff --git a/scm-ui/ui-webapp/public/locales/de/admin.json b/scm-ui/ui-webapp/public/locales/de/admin.json index ad413ccbc2..412a0b0318 100644 --- a/scm-ui/ui-webapp/public/locales/de/admin.json +++ b/scm-ui/ui-webapp/public/locales/de/admin.json @@ -88,6 +88,17 @@ }, "myCloudogu": { "connectionInfo": "Instanz ist mit myCloudogu verbunden.\nAccount: {{pluginCenterSubject}}", + "error": { + "info": "myCloudogu Authentifizierungsinformationen konnten nicht abgerufen werden. Klicken Sie, um Details zu sehen.", + "title": "Fehler" + }, + "failed": { + "info": "Verbindung zu myCloudogu mit Account {{pluginCenterSubject}} is fehlgeschlagen", + "message": "Die Verbindung der SCM-Manager Instanz mit <0>myCloudogu is fehlgeschlagen. Der Benutzer <1>{{subject}} konnte nicht authentifiziert werden.", + "button": { + "label": "Erneut mit <0>myCloudogu verbinden" + } + }, "login": { "button": { "label": "Mit <0>myCloudogu verbinden" diff --git a/scm-ui/ui-webapp/public/locales/en/admin.json b/scm-ui/ui-webapp/public/locales/en/admin.json index a44c6e6509..6c6606033b 100644 --- a/scm-ui/ui-webapp/public/locales/en/admin.json +++ b/scm-ui/ui-webapp/public/locales/en/admin.json @@ -88,6 +88,17 @@ }, "myCloudogu": { "connectionInfo": "Instance is connected to myCloudogu.\nAccount: {{pluginCenterSubject}}", + "error": { + "info": "Failed to retrieve myCloudogu authentication information. Click for more details.", + "title": "Error" + }, + "failed": { + "info": "Connection to myCloudogu failed for account {{pluginCenterSubject}}", + "message": "The connection of the SCM Manager instance with <0>myCloudogu failed. The user <1>{{subject}} could not be authenticated. Click Reconnect to restore the connection.", + "button": { + "label": "Reconnect to <0>myCloudogu" + } + }, "login": { "button": { "label": "Connect to <0>myCloudogu" diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/MyCloudoguBanner.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/MyCloudoguBanner.tsx index f06720566b..2db75647e3 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/MyCloudoguBanner.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/MyCloudoguBanner.tsx @@ -22,25 +22,84 @@ * SOFTWARE. */ -import { Button } from "@scm-manager/ui-components"; -import * as React from "react"; -import { FC } from "react"; -import styled from "styled-components"; +import React, { FC } from "react"; import { Trans, useTranslation } from "react-i18next"; - -const MyCloudoguBannerWrapper = styled.div` - border: 1px solid; -`; +import { Link, PluginCenterAuthenticationInfo } from "@scm-manager/ui-types"; +import classNames from "classnames"; +import styled from "styled-components"; +import { Button } from "@scm-manager/ui-components"; type Props = { - loginLink?: string; + info: PluginCenterAuthenticationInfo; }; -const MyCloudoguBanner: FC = ({ loginLink }) => { +const MyCloudoguBanner: FC = ({ info }) => { + const loginLink = (info._links.login as Link)?.href; + if (loginLink) { + return ; + } + + if (info.failed) { + const reconnectLink = (info._links.reconnect as Link)?.href; + if (reconnectLink) { + return ; + } + } + + return null; +}; + +type PropsWithLink = Props & { + link: string; +}; + +const FailedAuthentication: FC = ({ info, link }) => { const [t] = useTranslation("admin"); - return loginLink ? ( - - + + ); +}; + +type ContainerProps = { + className?: string; +}; + +const Container: FC = ({ className, children }) => ( + + {children} + +); + +const DivWithSolidBorder = styled.div` + border: 2px solid; +`; + +const Unauthenticated: FC = ({ link, info }) => { + const [t] = useTranslation("admin"); + return ( + +