From 2120a4ee029d9362e7ee88111e4ae632a7dad17b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 13 Aug 2019 08:23:37 +0200 Subject: [PATCH 01/10] implemented ui for login information --- scm-ui/public/locales/de/commons.json | 6 +- scm-ui/public/locales/en/commons.json | 7 +- scm-ui/src/components/InfoBox.js | 118 ++++++++++++++++++++++++ scm-ui/src/components/InfoItem.js | 8 ++ scm-ui/src/components/LoginForm.js | 120 ++++++++++++++++++++++++ scm-ui/src/components/LoginInfo.js | 54 +++++++++++ scm-ui/src/containers/Login.js | 127 +++++--------------------- 7 files changed, 332 insertions(+), 108 deletions(-) create mode 100644 scm-ui/src/components/InfoBox.js create mode 100644 scm-ui/src/components/InfoItem.js create mode 100644 scm-ui/src/components/LoginForm.js create mode 100644 scm-ui/src/components/LoginInfo.js diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index 3964863f46..cdbedd8b6b 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -5,7 +5,11 @@ "logo-alt": "SCM-Manager", "username-placeholder": "Benutzername", "password-placeholder": "Passwort", - "submit": "Anmelden" + "submit": "Anmelden", + "plugin": "Plugin", + "feature": "Feature", + "tip": "Tipp", + "loading": "Lade Daten ..." }, "logout": { "error": { diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index c57ef700ef..181fdc975c 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -5,7 +5,12 @@ "logo-alt": "SCM-Manager", "username-placeholder": "Your Username", "password-placeholder": "Your Password", - "submit": "Login" + "submit": "Login", + "plugin": "Plugin", + "feature": "Feature", + "tip": "Tip", + "loading": "Loading ...", + "error": "Error" }, "logout": { "error": { diff --git a/scm-ui/src/components/InfoBox.js b/scm-ui/src/components/InfoBox.js new file mode 100644 index 0000000000..95d487a210 --- /dev/null +++ b/scm-ui/src/components/InfoBox.js @@ -0,0 +1,118 @@ +//@flow +import * as React from "react"; +import { ErrorNotification } from "@scm-manager/ui-components"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { translate } from "react-i18next"; +import type { InfoItem } from "./InfoItem"; + +const styles = { + image: { + display: "flex", + alignItems: "center", + justifyContent: "center", + flexDirection: "column", + width: 160, + height: 160 + }, + icon: { + color: "#bff1e6" + }, + label: { + marginTop: "0.5em" + }, + content: { + marginLeft: "1.5em" + }, + link: { + width: "60%", + height: "200px", + position: "absolute", + cursor: "pointer", + zIndex: 20 + }, + "@media (max-width: 768px)": { + link: { + width: "100%", + } + } +}; + +type Props = { + type: "plugin" | "feature", + item?: InfoItem, + error?: Error, + + // context props + classes: any, + t: string => string +}; + +class InfoBox extends React.Component { + + renderBody = () => { + const { item, error, t } = this.props; + + const bodyClasses = classNames("media-content", "content", this.props.classes.content); + + if (error) { + return ( +
+

{t("login.error")}

+ +
+ ); + } + + const title = item ? item.title : t("login.loading"); + const summary = item ? item.summary : t("login.loading"); + + + return ( +
+

+ {title} +

+

{summary}

+
+ ); + + }; + + createHref = () => { + const { item } = this.props; + return item ? item._links.self.href : "#"; + }; + + createLink = () => { + const { classes } = this.props; + // eslint-disable-next-line jsx-a11y/anchor-has-content + return ; + }; + + render() { + const { type, classes, t } = this.props; + const icon = type === "plugin" ? "puzzle-piece" : "star"; + return ( + <> + {this.createLink()} +
+
+
+ +
{t("login." + type)}
+
{t("login.tip")}
+
+
+ {this.renderBody()} +
+ + ); + } + +} + +export default injectSheet(styles)(translate("commons")(InfoBox)); + + diff --git a/scm-ui/src/components/InfoItem.js b/scm-ui/src/components/InfoItem.js new file mode 100644 index 0000000000..b947bd3fce --- /dev/null +++ b/scm-ui/src/components/InfoItem.js @@ -0,0 +1,8 @@ +// @flow +import type { Link } from "@scm-manager/ui-types"; + +export type InfoItem = { + title: string, + summary: string, + _links: {[string]: Link} +}; diff --git a/scm-ui/src/components/LoginForm.js b/scm-ui/src/components/LoginForm.js new file mode 100644 index 0000000000..c1ad8c0ea5 --- /dev/null +++ b/scm-ui/src/components/LoginForm.js @@ -0,0 +1,120 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import { Image, ErrorNotification, InputField, SubmitButton, UnauthorizedError } from "@scm-manager/ui-components"; +import classNames from "classnames"; +import injectSheet from "react-jss"; + +const styles = { + avatar: { + marginTop: "-70px", + paddingBottom: "20px" + }, + avatarImage: { + border: "1px solid lightgray", + padding: "5px", + background: "#fff", + borderRadius: "50%", + width: "128px", + height: "128px" + }, + avatarSpacing: { + marginTop: "5rem" + } +}; + +type Props = { + error?: Error, + loading: boolean, + login: (username: string, password: string) => void, + + // context props + t: string => string, + classes: any +}; + +type State = { + username: string, + password: string +}; + +class LoginForm extends React.Component { + + constructor(props: Props) { + super(props); + this.state = { username: "", password: "" }; + } + + handleSubmit = (event: Event) => { + event.preventDefault(); + if (this.isValid()) { + this.props.login( + this.state.username, + this.state.password + ); + } + }; + + handleUsernameChange = (value: string) => { + this.setState({ username: value }); + }; + + handlePasswordChange = (value: string) => { + this.setState({ password: value }); + }; + + isValid() { + return this.state.username && this.state.password; + } + + areCredentialsInvalid() { + const { t, error } = this.props; + if (error instanceof UnauthorizedError) { + return new Error(t("errorNotification.wrongLoginCredentials")); + } else { + return error; + } + } + + render() { + const { loading, classes, t } = this.props; + return ( +
+

{t("login.title")}

+

{t("login.subtitle")}

+
+
+ {t("login.logo-alt")} +
+ +
+ + + + +
+
+ ); + } + +} + +export default injectSheet(styles)(translate("commons")(LoginForm)); + + diff --git a/scm-ui/src/components/LoginInfo.js b/scm-ui/src/components/LoginInfo.js new file mode 100644 index 0000000000..3bd80eaddc --- /dev/null +++ b/scm-ui/src/components/LoginInfo.js @@ -0,0 +1,54 @@ +//@flow +import React from "react"; +import InfoBox from "./InfoBox"; +import type { InfoItem } from "./InfoItem"; + +type Props = { +}; + +type State = { + plugin?: InfoItem, + feature?: InfoItem, + error?: Error +}; + +class LoginInfo extends React.Component { + + constructor(props: Props) { + super(props); + this.state = { + }; + } + + componentDidMount() { + fetch("https://login-info.scm-manager.org/api/v1/login-info") + .then(response => response.json()) + .then(info => { + this.setState({ + plugin: info.plugin, + feature: info.feature, + error: undefined + }); + }) + .catch(error => { + this.setState({ + error + }); + }); + } + + render() { + const { plugin, feature, error } = this.state; + return ( +
+ + +
+ ); + } + +} + +export default LoginInfo; + + diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index d14d9f5896..5d63698168 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -1,8 +1,6 @@ //@flow import React from "react"; import { Redirect, withRouter } from "react-router-dom"; -import injectSheet from "react-jss"; -import { translate } from "react-i18next"; import { login, isAuthenticated, @@ -10,148 +8,65 @@ import { getLoginFailure } from "../modules/auth"; import { connect } from "react-redux"; - -import { - InputField, - SubmitButton, - ErrorNotification, - Image, UnauthorizedError -} from "@scm-manager/ui-components"; -import classNames from "classnames"; import { getLoginLink } from "../modules/indexResource"; +import LoginForm from "../components/LoginForm"; +import LoginInfo from "../components/LoginInfo"; +import classNames from "classnames"; +import injectSheet from "react-jss"; const styles = { - avatar: { - marginTop: "-70px", - paddingBottom: "20px" - }, - avatarImage: { - border: "1px solid lightgray", - padding: "5px", - background: "#fff", - borderRadius: "50%", - width: "128px", - height: "128px" - }, - avatarSpacing: { - marginTop: "5rem" + section: { + paddingTop: "2em" } }; type Props = { authenticated: boolean, loading: boolean, - error: Error, + error?: Error, link: string, // dispatcher props login: (link: string, username: string, password: string) => void, // context props - t: string => string, classes: any, + t: string => string, from: any, location: any }; -type State = { - username: string, - password: string -}; +class Login extends React.Component { -class Login extends React.Component { - constructor(props: Props) { - super(props); - this.state = { username: "", password: "" }; - } - - handleUsernameChange = (value: string) => { - this.setState({ username: value }); + login = (username: string, password: string) => { + const { link, login } = this.props; + login(link, username, password); }; - handlePasswordChange = (value: string) => { - this.setState({ password: value }); - }; - - handleSubmit = (event: Event) => { - event.preventDefault(); - if (this.isValid()) { - this.props.login( - this.props.link, - this.state.username, - this.state.password - ); - } - }; - - isValid() { - return this.state.username && this.state.password; - } - - isInValid() { - return !this.isValid(); - } - - areCredentialsInvalid() { - const { t, error } = this.props; - if (error instanceof UnauthorizedError) { - return new Error(t("errorNotification.wrongLoginCredentials")); - } else { - return error; - } - } - renderRedirect = () => { const { from } = this.props.location.state || { from: { pathname: "/" } }; - return ; + return ; }; render() { - const { authenticated, loading, t, classes } = this.props; + const { authenticated, loading, error, classes } = this.props; if (authenticated) { return this.renderRedirect(); } return ( -
+
-
-
-

{t("login.title")}

-

{t("login.subtitle")}

-
-
- {t("login.logo-alt")} -
- -
- - - - -
+
+
+ +
- ); + ); } } @@ -179,6 +94,6 @@ const StyledLogin = injectSheet(styles)( connect( mapStateToProps, mapDispatchToProps - )(translate("commons")(Login)) + )(Login) ); export default withRouter(StyledLogin); From 3823c033b9a4fea0fb7d0abf5c6449cc83532667 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 13 Aug 2019 09:45:37 +0200 Subject: [PATCH 02/10] added configuration option for login info url --- .../sonia/scm/config/ScmConfiguration.java | 16 ++++++++ .../packages/ui-types/src/Config.js | 1 + scm-ui/public/locales/de/config.json | 6 ++- scm-ui/public/locales/en/config.json | 6 ++- .../src/admin/components/form/ConfigForm.js | 2 + .../admin/components/form/GeneralSettings.js | 15 ++++++++ scm-ui/src/components/LoginInfo.js | 4 +- scm-ui/src/containers/Login.js | 18 ++++++--- scm-ui/src/modules/indexResource.js | 4 ++ .../sonia/scm/api/v2/resources/ConfigDto.java | 1 + .../api/v2/resources/IndexDtoGenerator.java | 11 +++++- ...ConfigDtoToScmConfigurationMapperTest.java | 2 + .../api/v2/resources/IndexResourceTest.java | 37 +++++++++++++++++-- ...ScmConfigurationToConfigDtoMapperTest.java | 2 + 14 files changed, 111 insertions(+), 14 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 8d3db8b348..d23bbaf07d 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -75,6 +75,11 @@ public class ScmConfiguration implements Configuration { public static final String DEFAULT_PLUGINURL = "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false"; + /** + * Default url for login information (plugin and feature tips on the login page). + */ + public static final String DEFAULT_LOGIN_INFO_URL = "https://login-info.scm-manager.org/api/v1/login-info"; + /** * Default plugin url from version 1.0 */ @@ -177,6 +182,9 @@ public class ScmConfiguration implements Configuration { @XmlElement(name = "namespace-strategy") private String namespaceStrategy = "UsernameNamespaceStrategy"; + @XmlElement(name = "login-info-url") + private String loginInfoUrl = DEFAULT_LOGIN_INFO_URL; + /** * Calls the {@link sonia.scm.ConfigChangedListener#configChanged(Object)} @@ -216,6 +224,7 @@ public class ScmConfiguration implements Configuration { this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; this.enabledXsrfProtection = other.enabledXsrfProtection; this.namespaceStrategy = other.namespaceStrategy; + this.loginInfoUrl = other.loginInfoUrl; } /** @@ -350,6 +359,9 @@ public class ScmConfiguration implements Configuration { return namespaceStrategy; } + public String getLoginInfoUrl() { + return loginInfoUrl; + } /** * Returns true if failed authenticators are skipped. @@ -477,6 +489,10 @@ public class ScmConfiguration implements Configuration { this.namespaceStrategy = namespaceStrategy; } + public void setLoginInfoUrl(String loginInfoUrl) { + this.loginInfoUrl = loginInfoUrl; + } + @Override // Only for permission checks, don't serialize to XML @XmlTransient diff --git a/scm-ui-components/packages/ui-types/src/Config.js b/scm-ui-components/packages/ui-types/src/Config.js index 0e82076848..5a9522585f 100644 --- a/scm-ui-components/packages/ui-types/src/Config.js +++ b/scm-ui-components/packages/ui-types/src/Config.js @@ -21,5 +21,6 @@ export type Config = { loginAttemptLimitTimeout: number, enabledXsrfProtection: boolean, namespaceStrategy: string, + loginInfoUrl: string, _links: Links }; diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index 20977583ac..f00b3e2357 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -43,7 +43,8 @@ "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "plugin-url": "Plugin URL", "enabled-xsrf-protection": "XSRF Protection aktivieren", - "namespace-strategy": "Namespace Strategie" + "namespace-strategy": "Namespace Strategie", + "login-info-url": "Login Info URL", }, "validation": { "date-format-invalid": "Das Datumsformat ist ungültig", @@ -73,6 +74,7 @@ "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", - "nameSpaceStrategyHelpText": "Strategie für Namespaces." + "nameSpaceStrategyHelpText": "Strategie für Namespaces.", + "loginInfoUrlHelpText": "URL zu den Login Informationen (Plugin und Feature Tipps auf der Login Seite)." } } diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 1aa13a3150..62215df827 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -43,7 +43,8 @@ "skip-failed-authenticators": "Skip Failed Authenticators", "plugin-url": "Plugin URL", "enabled-xsrf-protection": "Enabled XSRF Protection", - "namespace-strategy": "Namespace Strategy" + "namespace-strategy": "Namespace Strategy", + "login-info-url": "Login Info URL" }, "validation": { "date-format-invalid": "The date format is not valid", @@ -73,6 +74,7 @@ "proxyUserHelpText": "The username for the proxy server authentication.", "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", - "nameSpaceStrategyHelpText": "The namespace strategy." + "nameSpaceStrategyHelpText": "The namespace strategy.", + "loginInfoUrlHelpText": "URL to login information (plugin and feature tips at login page)." } } diff --git a/scm-ui/src/admin/components/form/ConfigForm.js b/scm-ui/src/admin/components/form/ConfigForm.js index 20e2db2599..25a24c4d28 100644 --- a/scm-ui/src/admin/components/form/ConfigForm.js +++ b/scm-ui/src/admin/components/form/ConfigForm.js @@ -54,6 +54,7 @@ class ConfigForm extends React.Component { loginAttemptLimitTimeout: 0, enabledXsrfProtection: true, namespaceStrategy: "", + loginInfoUrl: "", _links: {} }, showNotification: false, @@ -119,6 +120,7 @@ class ConfigForm extends React.Component { {noPermissionNotification} { const { t, realmDescription, + loginInfoUrl, enabledXsrfProtection, namespaceStrategy, hasUpdatePermission, @@ -57,6 +59,15 @@ class GeneralSettings extends React.Component {
+
+ +
{ ); } + handleLoginInfoUrlChange = (value: string) => { + this.props.onChange(true, value, "loginInfoUrl"); + }; + handleRealmDescriptionChange = (value: string) => { this.props.onChange(true, value, "realmDescription"); }; diff --git a/scm-ui/src/components/LoginInfo.js b/scm-ui/src/components/LoginInfo.js index 3bd80eaddc..63ce6f3dbe 100644 --- a/scm-ui/src/components/LoginInfo.js +++ b/scm-ui/src/components/LoginInfo.js @@ -4,6 +4,7 @@ import InfoBox from "./InfoBox"; import type { InfoItem } from "./InfoItem"; type Props = { + loginInfoLink: string }; type State = { @@ -21,7 +22,8 @@ class LoginInfo extends React.Component { } componentDidMount() { - fetch("https://login-info.scm-manager.org/api/v1/login-info") + const { loginInfoLink } = this.props; + fetch(loginInfoLink) .then(response => response.json()) .then(info => { this.setState({ diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index 5d63698168..4b30f9cb0f 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -8,7 +8,7 @@ import { getLoginFailure } from "../modules/auth"; import { connect } from "react-redux"; -import { getLoginLink } from "../modules/indexResource"; +import { getLoginLink, getLoginInfoLink } from "../modules/indexResource"; import LoginForm from "../components/LoginForm"; import LoginInfo from "../components/LoginInfo"; import classNames from "classnames"; @@ -25,6 +25,7 @@ type Props = { loading: boolean, error?: Error, link: string, + loginInfoLink?: string, // dispatcher props login: (link: string, username: string, password: string) => void, @@ -49,19 +50,24 @@ class Login extends React.Component { }; render() { - const { authenticated, loading, error, classes } = this.props; + const { authenticated, loginInfoLink, loading, error, classes } = this.props; if (authenticated) { return this.renderRedirect(); } + let loginInfo; + if (loginInfoLink) { + loginInfo = + } + return (
-
+
- + {loginInfo}
@@ -75,11 +81,13 @@ const mapStateToProps = state => { const loading = isLoginPending(state); const error = getLoginFailure(state); const link = getLoginLink(state); + const loginInfoLink = getLoginInfoLink(state); return { authenticated, loading, error, - link + link, + loginInfoLink }; }; diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index 9bfa620674..9676faffba 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -168,6 +168,10 @@ export function getSvnConfigLink(state: Object) { return getLink(state, "svnConfig"); } +export function getLoginInfoLink(state: Object) { + return getLink(state, "loginInfo"); +} + export function getUserAutoCompleteLink(state: Object): string { const link = getLinkCollection(state, "autocomplete").find( i => i.name === "users" diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index 1852d6fdc4..30d936d4c5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -32,6 +32,7 @@ public class ConfigDto extends HalRepresentation { private long loginAttemptLimitTimeout; private boolean enabledXsrfProtection; private String namespaceStrategy; + private String loginInfoUrl; @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index c7b52861dc..cace57577c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; @@ -7,6 +8,7 @@ import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; import sonia.scm.SCMContextProvider; import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.config.ScmConfiguration; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.PluginPermissions; import sonia.scm.repository.RepositoryRolePermissions; @@ -23,11 +25,13 @@ public class IndexDtoGenerator extends HalAppenderMapper { private final ResourceLinks resourceLinks; private final SCMContextProvider scmContextProvider; + private final ScmConfiguration configuration; @Inject - public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) { + public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider, ScmConfiguration configuration) { this.resourceLinks = resourceLinks; this.scmContextProvider = scmContextProvider; + this.configuration = configuration; } public IndexDto generate() { @@ -36,6 +40,11 @@ public class IndexDtoGenerator extends HalAppenderMapper { builder.self(resourceLinks.index().self()); builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self())); + String loginInfoUrl = configuration.getLoginInfoUrl(); + if (!Strings.isNullOrEmpty(loginInfoUrl)) { + builder.single(link("loginInfo", loginInfoUrl)); + } + if (SecurityUtils.getSubject().isAuthenticated()) { builder.single( link("me", resourceLinks.me().self()), diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java index dd09e50266..e7ae446185 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java @@ -50,6 +50,7 @@ public class ConfigDtoToScmConfigurationMapperTest { assertEquals(40 , config.getLoginAttemptLimitTimeout()); assertTrue(config.isEnabledXsrfProtection()); assertEquals("username", config.getNamespaceStrategy()); + assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl()); } private ConfigDto createDefaultDto() { @@ -73,6 +74,7 @@ public class ConfigDtoToScmConfigurationMapperTest { configDto.setLoginAttemptLimitTimeout(40); configDto.setEnabledXsrfProtection(true); configDto.setNamespaceStrategy("username"); + configDto.setLoginInfoUrl("https://scm-manager.org/login-info"); return configDto; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java index 93099cf5ea..9dfa5fca28 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java @@ -3,9 +3,11 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import org.assertj.core.api.Assertions; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import sonia.scm.SCMContextProvider; +import sonia.scm.config.ScmConfiguration; import java.net.URI; import java.util.Optional; @@ -19,9 +21,22 @@ public class IndexResourceTest { @Rule public final ShiroRule shiroRule = new ShiroRule(); - private final SCMContextProvider scmContextProvider = mock(SCMContextProvider.class); - private final IndexDtoGenerator indexDtoGenerator = new IndexDtoGenerator(ResourceLinksMock.createMock(URI.create("/")), scmContextProvider); - private final IndexResource indexResource = new IndexResource(indexDtoGenerator); + private ScmConfiguration configuration; + private SCMContextProvider scmContextProvider; + private IndexResource indexResource; + + + @Before + public void setUpObjectUnderTest() { + this.configuration = new ScmConfiguration(); + this.scmContextProvider = mock(SCMContextProvider.class); + IndexDtoGenerator generator = new IndexDtoGenerator( + ResourceLinksMock.createMock(URI.create("/")), + scmContextProvider, + configuration + ); + this.indexResource = new IndexResource(generator); + } @Test public void shouldRenderLoginUrlsForUnauthenticatedRequest() { @@ -30,6 +45,22 @@ public class IndexResourceTest { Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent); } + @Test + public void shouldRenderLoginInfoUrl() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isPresent(); + } + + @Test + public void shouldNotRenderLoginInfoUrlWhenNoUrlIsConfigured() { + configuration.setLoginInfoUrl(""); + + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isNotPresent(); + } + @Test public void shouldRenderSelfLinkForUnauthenticatedRequest() { IndexDto index = indexResource.getIndex(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index ee940a9721..6ae6d5d2f1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -80,6 +80,7 @@ public class ScmConfigurationToConfigDtoMapperTest { assertEquals(2 , dto.getLoginAttemptLimitTimeout()); assertTrue(dto.isEnabledXsrfProtection()); assertEquals("username", dto.getNamespaceStrategy()); + assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); @@ -118,6 +119,7 @@ public class ScmConfigurationToConfigDtoMapperTest { config.setLoginAttemptLimitTimeout(2); config.setEnabledXsrfProtection(true); config.setNamespaceStrategy("username"); + config.setLoginInfoUrl("https://scm-manager.org/login-info"); return config; } From 372e629dfce8f80936a8135da07d01818696bf8c Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 15 Aug 2019 10:08:48 +0200 Subject: [PATCH 03/10] do not show login tips, if they could not fetched --- scm-ui/src/components/InfoBox.js | 28 ++++----------- scm-ui/src/components/LoginForm.js | 4 +-- scm-ui/src/components/LoginInfo.js | 56 ++++++++++++++++++++++-------- scm-ui/src/containers/Login.js | 13 ++----- 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/scm-ui/src/components/InfoBox.js b/scm-ui/src/components/InfoBox.js index 95d487a210..794ad24684 100644 --- a/scm-ui/src/components/InfoBox.js +++ b/scm-ui/src/components/InfoBox.js @@ -33,15 +33,14 @@ const styles = { }, "@media (max-width: 768px)": { link: { - width: "100%", + width: "100%" } } }; type Props = { type: "plugin" | "feature", - item?: InfoItem, - error?: Error, + item: InfoItem, // context props classes: any, @@ -51,27 +50,16 @@ type Props = { class InfoBox extends React.Component { renderBody = () => { - const { item, error, t } = this.props; + const { item, t } = this.props; const bodyClasses = classNames("media-content", "content", this.props.classes.content); - - if (error) { - return ( -
-

{t("login.error")}

- -
- ); - } - const title = item ? item.title : t("login.loading"); const summary = item ? item.summary : t("login.loading"); - return (

- {title} + {title}

{summary}

@@ -79,15 +67,11 @@ class InfoBox extends React.Component { }; - createHref = () => { - const { item } = this.props; - return item ? item._links.self.href : "#"; - }; createLink = () => { - const { classes } = this.props; + const { item, classes } = this.props; // eslint-disable-next-line jsx-a11y/anchor-has-content - return ; + return ; }; render() { diff --git a/scm-ui/src/components/LoginForm.js b/scm-ui/src/components/LoginForm.js index c1ad8c0ea5..95d4c0b7d3 100644 --- a/scm-ui/src/components/LoginForm.js +++ b/scm-ui/src/components/LoginForm.js @@ -26,7 +26,7 @@ const styles = { type Props = { error?: Error, loading: boolean, - login: (username: string, password: string) => void, + loginHandler: (username: string, password: string) => void, // context props t: string => string, @@ -48,7 +48,7 @@ class LoginForm extends React.Component { handleSubmit = (event: Event) => { event.preventDefault(); if (this.isValid()) { - this.props.login( + this.props.loginHandler( this.state.username, this.state.password ); diff --git a/scm-ui/src/components/LoginInfo.js b/scm-ui/src/components/LoginInfo.js index 63ce6f3dbe..a7e488e646 100644 --- a/scm-ui/src/components/LoginInfo.js +++ b/scm-ui/src/components/LoginInfo.js @@ -2,15 +2,24 @@ import React from "react"; import InfoBox from "./InfoBox"; import type { InfoItem } from "./InfoItem"; +import LoginForm from "./LoginForm"; +import { Loading } from "@scm-manager/ui-components"; type Props = { - loginInfoLink: string + loginInfoLink?: string, + loading?: boolean, + error?: Error, + loginHandler: (username: string, password: string) => void, +}; + +type LoginInfoResponse = { + plugin?: InfoItem, + feature?: InfoItem }; type State = { - plugin?: InfoItem, - feature?: InfoItem, - error?: Error + info?: LoginInfoResponse, + loading?: boolean, }; class LoginInfo extends React.Component { @@ -18,34 +27,53 @@ class LoginInfo extends React.Component { constructor(props: Props) { super(props); this.state = { + loading: !!props.loginInfoLink }; } componentDidMount() { const { loginInfoLink } = this.props; + if (!loginInfoLink) { + return; + } fetch(loginInfoLink) .then(response => response.json()) .then(info => { this.setState({ - plugin: info.plugin, - feature: info.feature, - error: undefined + info, + loading: false }); }) - .catch(error => { + .catch(() => { this.setState({ - error + loading: false }); }); } + createInfoPanel = (info: LoginInfoResponse) => ( +
+ + +
+ ); + render() { - const { plugin, feature, error } = this.state; + const { info, loading } = this.state; + if (loading) { + return ; + } + + let infoPanel; + if (info) { + infoPanel = this.createInfoPanel(info); + } + return ( -
- - -
+ <> + + {infoPanel} + ); } diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index 4b30f9cb0f..f8246ab88b 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -9,7 +9,6 @@ import { } from "../modules/auth"; import { connect } from "react-redux"; import { getLoginLink, getLoginInfoLink } from "../modules/indexResource"; -import LoginForm from "../components/LoginForm"; import LoginInfo from "../components/LoginInfo"; import classNames from "classnames"; import injectSheet from "react-jss"; @@ -39,7 +38,7 @@ type Props = { class Login extends React.Component { - login = (username: string, password: string) => { + handleLogin = (username: string, password: string): void => { const { link, login } = this.props; login(link, username, password); }; @@ -50,24 +49,18 @@ class Login extends React.Component { }; render() { - const { authenticated, loginInfoLink, loading, error, classes } = this.props; + const { authenticated, classes, ...restProps } = this.props; if (authenticated) { return this.renderRedirect(); } - let loginInfo; - if (loginInfoLink) { - loginInfo = - } - return (
- - {loginInfo} +
From 0859353f46408b7f0f5272d7cbad4c2c28006db8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 15 Aug 2019 10:20:59 +0200 Subject: [PATCH 04/10] use a timeout of 1s for fetching login info --- scm-ui/src/components/LoginInfo.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/scm-ui/src/components/LoginInfo.js b/scm-ui/src/components/LoginInfo.js index a7e488e646..3f7ea5ede3 100644 --- a/scm-ui/src/components/LoginInfo.js +++ b/scm-ui/src/components/LoginInfo.js @@ -31,19 +31,32 @@ class LoginInfo extends React.Component { }; } - componentDidMount() { - const { loginInfoLink } = this.props; - if (!loginInfoLink) { - return; - } - fetch(loginInfoLink) + fetchLoginInfo = (url: string) => { + return fetch(url) .then(response => response.json()) .then(info => { this.setState({ info, loading: false }); - }) + }); + }; + + timeout = (ms: number, promise: Promise) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("timeout during fetch of login info")); + }, ms); + promise.then(resolve, reject); + }); + }; + + componentDidMount() { + const { loginInfoLink } = this.props; + if (!loginInfoLink) { + return; + } + this.timeout(1000, this.fetchLoginInfo(loginInfoLink)) .catch(() => { this.setState({ loading: false From e870d90b728b54b8ba2b5cb741536cfd288917f4 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 15 Aug 2019 10:30:48 +0200 Subject: [PATCH 05/10] simplify info link handling --- scm-ui/src/components/InfoBox.js | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/scm-ui/src/components/InfoBox.js b/scm-ui/src/components/InfoBox.js index 794ad24684..3f1a4fe32a 100644 --- a/scm-ui/src/components/InfoBox.js +++ b/scm-ui/src/components/InfoBox.js @@ -25,16 +25,8 @@ const styles = { marginLeft: "1.5em" }, link: { - width: "60%", - height: "200px", - position: "absolute", - cursor: "pointer", - zIndex: 20 - }, - "@media (max-width: 768px)": { - link: { - width: "100%" - } + display: "block", + marginBottom: "1.5rem" } }; @@ -58,28 +50,18 @@ class InfoBox extends React.Component { return (
-

- {title} -

+

{title}

{summary}

); }; - - createLink = () => { - const { item, classes } = this.props; - // eslint-disable-next-line jsx-a11y/anchor-has-content - return ; - }; - render() { - const { type, classes, t } = this.props; + const { item, type, classes, t } = this.props; const icon = type === "plugin" ? "puzzle-piece" : "star"; return ( - <> - {this.createLink()} +
{
{this.renderBody()}
- +
); } From 71adb69fe49d7e98a6f5dab7841e4ea610e7e961 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 15 Aug 2019 10:31:01 +0200 Subject: [PATCH 06/10] adjust help texts for login info --- scm-ui/public/locales/de/config.json | 4 ++-- scm-ui/public/locales/en/config.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index f00b3e2357..a94965fa68 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -44,7 +44,7 @@ "plugin-url": "Plugin URL", "enabled-xsrf-protection": "XSRF Protection aktivieren", "namespace-strategy": "Namespace Strategie", - "login-info-url": "Login Info URL", + "login-info-url": "Login Info URL" }, "validation": { "date-format-invalid": "Das Datumsformat ist ungültig", @@ -75,6 +75,6 @@ "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", "nameSpaceStrategyHelpText": "Strategie für Namespaces.", - "loginInfoUrlHelpText": "URL zu den Login Informationen (Plugin und Feature Tipps auf der Login Seite)." + "loginInfoUrlHelpText": "URL zu der Login Information (Plugin und Feature Tipps auf der Login Seite). Um die Login Information zu deaktivieren, kann das Feld leer gelassen werden." } } diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 62215df827..894d0563ba 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -75,6 +75,6 @@ "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", "nameSpaceStrategyHelpText": "The namespace strategy.", - "loginInfoUrlHelpText": "URL to login information (plugin and feature tips at login page)." + "loginInfoUrlHelpText": "URL to login information (plugin and feature tips at login page). If this is omitted, no login information will be displayed." } } From a698444afe1cfc09763aac840a0d9547629594cd Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 15 Aug 2019 10:40:38 +0200 Subject: [PATCH 07/10] remove unused import --- scm-ui/src/components/InfoBox.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scm-ui/src/components/InfoBox.js b/scm-ui/src/components/InfoBox.js index 3f1a4fe32a..f6fc170826 100644 --- a/scm-ui/src/components/InfoBox.js +++ b/scm-ui/src/components/InfoBox.js @@ -1,6 +1,5 @@ //@flow import * as React from "react"; -import { ErrorNotification } from "@scm-manager/ui-components"; import injectSheet from "react-jss"; import classNames from "classnames"; import { translate } from "react-i18next"; From d4d99cb7d58ba7f1c0cf01102626bc7e5e098e4c Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 15 Aug 2019 12:34:37 +0000 Subject: [PATCH 08/10] Close branch feature/login_info From aa1452ab851164eb48b16c4306e329964965acbd Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 16 Aug 2019 15:56:15 +0200 Subject: [PATCH 09/10] Let git implement DIFF_RESULT command --- .../scm/repository/spi/GitRepositoryServiceProvider.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index f4b19d1e85..6870c85dea 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -59,7 +59,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider Command.BLAME, Command.BROWSE, Command.CAT, - Command.DIFF, + Command.DIFF, + Command.DIFF_RESULT, Command.LOG, Command.TAGS, Command.BRANCHES, @@ -168,6 +169,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitDiffCommand(context, repository); } + @Override + public DiffResultCommand getDiffResultCommand() { + return new GitDiffResultCommand(context, repository); + } + /** * Method description * From bd5a8ba508d386650c411f8b14f285ae8aaeefdd Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 19 Aug 2019 09:23:38 +0200 Subject: [PATCH 10/10] Add css class to set cursor pointer --- scm-ui/styles/scm.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index ddb2a748e5..48d66e9768 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -526,5 +526,9 @@ form .field:not(.is-grouped) { } } +// cursor +.has-cursor-pointer { + cursor: pointer; +} @import "bulma-popover/css/bulma-popover";