mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-16 02:06:18 +01:00
Add login form extension point (#2088)
One of our users would like to create a new plugin that provides alternative methods of authentication. For that purpose, we need an additional extension point that can either replace the login form entirely, or extend it by adding login buttons. By default, the extension point simply renders the original login form.
This commit is contained in:
committed by
GitHub
parent
f647e06d3c
commit
8c41fab30d
@@ -44,7 +44,7 @@ import {
|
||||
RepositoryTypeCollection,
|
||||
Tag,
|
||||
User,
|
||||
ContentType
|
||||
ContentType,
|
||||
} from "@scm-manager/ui-types";
|
||||
import { ExtensionPointDefinition } from "./binder";
|
||||
import { RenderableExtensionPointDefinition, SimpleRenderableDynamicExtensionPointDefinition } from "./ExtensionPoint";
|
||||
@@ -650,3 +650,5 @@ export type FileViewActionBarOverflowMenu = ExtensionPointDefinition<
|
||||
ActionMenuProps | ModalMenuProps | LinkMenuProps,
|
||||
ContentActionExtensionProps
|
||||
>;
|
||||
|
||||
export type LoginForm = RenderableExtensionPointDefinition<"login.form">;
|
||||
|
||||
@@ -25,7 +25,7 @@ import * as React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { InfoItem } from "@scm-manager/ui-types";
|
||||
import { InfoItem, Link } from "@scm-manager/ui-types";
|
||||
import { devices, Icon } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
@@ -70,7 +70,7 @@ class InfoBox extends React.Component<Props> {
|
||||
const { item, type, t } = this.props;
|
||||
const icon = type === "plugin" ? "puzzle-piece" : "star";
|
||||
return (
|
||||
<a className="is-block mb-5" href={item._links.self.href}>
|
||||
<a className="is-block mb-5" href={(item?._links?.self as Link)?.href}>
|
||||
<InfoBoxWrapper className="box media">
|
||||
<figure className="media-left">
|
||||
<FixedSizedIconWrapper
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
*/
|
||||
import React, { FormEvent } from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { ErrorNotification, Image, InputField, SubmitButton, UnauthorizedError } from "@scm-manager/ui-components";
|
||||
import { ErrorNotification, InputField, SubmitButton, UnauthorizedError } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
error?: Error | null;
|
||||
@@ -37,33 +36,12 @@ type State = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
const TopMarginBox = styled.div`
|
||||
margin-top: 5rem;
|
||||
`;
|
||||
|
||||
const AvatarWrapper = styled.figure`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: -70px auto 20px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background: var(--scm-white-color);
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
const AvatarImage = styled(Image)`
|
||||
width: 75%;
|
||||
margin-left: 0.25rem;
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
class LoginForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: "",
|
||||
password: ""
|
||||
password: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,13 +54,13 @@ class LoginForm extends React.Component<Props, State> {
|
||||
|
||||
handleUsernameChange = (value: string) => {
|
||||
this.setState({
|
||||
username: value
|
||||
username: value,
|
||||
});
|
||||
};
|
||||
|
||||
handlePasswordChange = (value: string) => {
|
||||
this.setState({
|
||||
password: value
|
||||
password: value,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -92,7 +70,7 @@ class LoginForm extends React.Component<Props, State> {
|
||||
|
||||
areCredentialsInvalid() {
|
||||
const { t, error } = this.props;
|
||||
if (error instanceof UnauthorizedError) {
|
||||
if (error && error instanceof UnauthorizedError) {
|
||||
return new Error(t("errorNotification.wrongLoginCredentials"));
|
||||
} else {
|
||||
return error;
|
||||
@@ -102,13 +80,7 @@ class LoginForm extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { loading, t } = this.props;
|
||||
return (
|
||||
<div className="column is-4 box has-text-centered has-background-secondary-less">
|
||||
<h3 className="title">{t("login.title")}</h3>
|
||||
<p className="subtitle">{t("login.subtitle")}</p>
|
||||
<TopMarginBox className="box">
|
||||
<AvatarWrapper>
|
||||
<AvatarImage src="/images/blibSmallLightBackground.svg" alt={t("login.logo-alt")} />
|
||||
</AvatarWrapper>
|
||||
<>
|
||||
<ErrorNotification error={this.areCredentialsInvalid()} />
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<InputField
|
||||
@@ -125,8 +97,7 @@ class LoginForm extends React.Component<Props, State> {
|
||||
/>
|
||||
<SubmitButton label={t("login.submit")} fullWidth={true} loading={loading} testId="login-button" />
|
||||
</form>
|
||||
</TopMarginBox>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
78
scm-ui/ui-webapp/src/components/LoginInfo.test.tsx
Normal file
78
scm-ui/ui-webapp/src/components/LoginInfo.test.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 * as TestRenderer from "react-test-renderer";
|
||||
import { LoginInfo as LoginInfoType } from "@scm-manager/ui-types";
|
||||
|
||||
import "@scm-manager/ui-tests";
|
||||
import LoginInfo from "./LoginInfo";
|
||||
import { Binder, BinderContext, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
|
||||
jest.mock("@scm-manager/ui-api", () => ({
|
||||
useLoginInfo: jest.fn(() => ({
|
||||
isLoading: false,
|
||||
data: {
|
||||
_links: {},
|
||||
feature: { title: "Test", summary: "Test" },
|
||||
plugin: { summary: "Test", title: "Test" },
|
||||
} as LoginInfoType,
|
||||
})),
|
||||
urls: {
|
||||
getPageFromMatch: jest.fn(() => 1),
|
||||
withContextPath: jest.fn((it) => it),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("LoginInfo", () => {
|
||||
const withBinder = (ui, binder) => <BinderContext.Provider value={binder}>{ui}</BinderContext.Provider>;
|
||||
|
||||
it("should render login page", () => {
|
||||
const loginHandler = jest.fn();
|
||||
const binder = new Binder("test");
|
||||
const reactTestRenderer = TestRenderer.create(withBinder(<LoginInfo loginHandler={loginHandler} />, binder));
|
||||
expect(reactTestRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render extension", () => {
|
||||
const loginHandler = jest.fn();
|
||||
const binder = new Binder("test");
|
||||
binder.bind<extensionPoints.LoginForm>("login.form", () => <button>Login with OAuth2</button>);
|
||||
const reactTestRenderer = TestRenderer.create(withBinder(<LoginInfo loginHandler={loginHandler} />, binder));
|
||||
expect(reactTestRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render extension with login form", () => {
|
||||
const loginHandler = jest.fn();
|
||||
const binder = new Binder("test");
|
||||
binder.bind<extensionPoints.LoginForm>("login.form", ({ children }) => (
|
||||
<>
|
||||
{children}
|
||||
<button>Login with OAuth2</button>
|
||||
</>
|
||||
));
|
||||
const reactTestRenderer = TestRenderer.create(withBinder(<LoginInfo loginHandler={loginHandler} />, binder));
|
||||
expect(reactTestRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -21,125 +21,81 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
import InfoBox from "./InfoBox";
|
||||
import { LoginInfo as LoginInfoResponse } from "@scm-manager/ui-types";
|
||||
import LoginForm from "./LoginForm";
|
||||
import { Loading } from "@scm-manager/ui-components";
|
||||
import { Image, Loading } from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import styled from "styled-components";
|
||||
import { useLoginInfo } from "@scm-manager/ui-api";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const TopMarginBox = styled.div`
|
||||
margin-top: 5rem;
|
||||
`;
|
||||
|
||||
const AvatarWrapper = styled.figure`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: -70px auto 20px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background: var(--scm-white-color);
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
const AvatarImage = styled(Image)`
|
||||
width: 75%;
|
||||
margin-left: 0.25rem;
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* @deprecated Unused because the component now uses {@link useLoginInfo} internally.
|
||||
*/
|
||||
loginInfoLink?: string;
|
||||
loading?: boolean;
|
||||
error?: Error | null;
|
||||
loginHandler: (username: string, password: string) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
info?: LoginInfoResponse;
|
||||
loading?: boolean;
|
||||
};
|
||||
const LoginInfo: FC<Props> = (props) => {
|
||||
const { isLoading: isLoadingLoginInfo, data: info } = useLoginInfo();
|
||||
const [t] = useTranslation("commons");
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type NoOpErrorBoundaryProps = {};
|
||||
|
||||
type NoOpErrorBoundaryState = {
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
class NoOpErrorBoundary extends React.Component<NoOpErrorBoundaryProps, NoOpErrorBoundaryState> {
|
||||
constructor(props: NoOpErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (console && console.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("failed to render login info", error);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
class LoginInfo extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: !!props.loginInfoLink
|
||||
};
|
||||
}
|
||||
|
||||
fetchLoginInfo = (url: string) => {
|
||||
return fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(info => {
|
||||
this.setState({
|
||||
info,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
timeout = (ms: number, promise: Promise<void>) => {
|
||||
return new Promise<void>((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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createInfoPanel = (info: LoginInfoResponse) => (
|
||||
<NoOpErrorBoundary>
|
||||
<div className="column is-7 is-offset-1 p-0">
|
||||
<InfoBox item={info.feature} type="feature" />
|
||||
<InfoBox item={info.plugin} type="plugin" />
|
||||
</div>
|
||||
</NoOpErrorBoundary>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { info, loading } = this.state;
|
||||
if (loading) {
|
||||
if (isLoadingLoginInfo) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
let infoPanel;
|
||||
if (info) {
|
||||
infoPanel = this.createInfoPanel(info);
|
||||
infoPanel = (
|
||||
<div className="column is-7 is-offset-1 p-0">
|
||||
<InfoBox item={info.feature} type="feature" />
|
||||
<InfoBox item={info.plugin} type="plugin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoginForm {...this.props} />
|
||||
<div className="column is-4 box has-text-centered has-background-secondary-less">
|
||||
<h3 className="title">{t("login.title")}</h3>
|
||||
<p className="subtitle">{t("login.subtitle")}</p>
|
||||
<TopMarginBox className="box">
|
||||
<AvatarWrapper>
|
||||
<AvatarImage src="/images/blibSmallLightBackground.svg" alt={t("login.logo-alt")} />
|
||||
</AvatarWrapper>
|
||||
<ExtensionPoint<extensionPoints.LoginForm> name="login.form" props={{}}>
|
||||
<LoginForm {...props} />
|
||||
</ExtensionPoint>
|
||||
</TopMarginBox>
|
||||
</div>
|
||||
{infoPanel}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default LoginInfo;
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoginInfo should render extension 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="column is-4 box has-text-centered has-background-secondary-less"
|
||||
>
|
||||
<h3
|
||||
className="title"
|
||||
>
|
||||
login.title
|
||||
</h3>
|
||||
<p
|
||||
className="subtitle"
|
||||
>
|
||||
login.subtitle
|
||||
</p>
|
||||
<div
|
||||
className="LoginInfo__TopMarginBox-sc-dmmy9i-0 hXYZcL box"
|
||||
>
|
||||
<figure
|
||||
className="LoginInfo__AvatarWrapper-sc-dmmy9i-1 ikoLxp"
|
||||
>
|
||||
<img
|
||||
alt="login.logo-alt"
|
||||
className="LoginInfo__AvatarImage-sc-dmmy9i-2 gFcMrK"
|
||||
src="/images/blibSmallLightBackground.svg"
|
||||
/>
|
||||
</figure>
|
||||
<button>
|
||||
Login with OAuth2
|
||||
</button>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
className="column is-7 is-offset-1 p-0"
|
||||
>
|
||||
<a
|
||||
className="is-block mb-5"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__InfoBoxWrapper-sc-qh4uzj-2 dgRYgc box media"
|
||||
>
|
||||
<figure
|
||||
className="media-left"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__FixedSizedIconWrapper-sc-qh4uzj-0 bCVWB image box has-text-weight-bold has-text-secondary-least has-background-info is-flex is-flex-direction-column is-justify-content-center is-align-items-center"
|
||||
>
|
||||
<i
|
||||
aria-label=""
|
||||
className="fas fa-fw fa-star has-text-white mb-2 fa-2x"
|
||||
onKeyPress={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.feature
|
||||
</div>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.tip
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div
|
||||
className="InfoBox__ContentWrapper-sc-qh4uzj-1 eiUgVo media-content content ml-5"
|
||||
>
|
||||
<h4
|
||||
className="has-text-link"
|
||||
>
|
||||
Test
|
||||
</h4>
|
||||
<p>
|
||||
Test
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
className="is-block mb-5"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__InfoBoxWrapper-sc-qh4uzj-2 dgRYgc box media"
|
||||
>
|
||||
<figure
|
||||
className="media-left"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__FixedSizedIconWrapper-sc-qh4uzj-0 bCVWB image box has-text-weight-bold has-text-secondary-least has-background-info is-flex is-flex-direction-column is-justify-content-center is-align-items-center"
|
||||
>
|
||||
<i
|
||||
aria-label=""
|
||||
className="fas fa-fw fa-puzzle-piece has-text-white mb-2 fa-2x"
|
||||
onKeyPress={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.plugin
|
||||
</div>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.tip
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div
|
||||
className="InfoBox__ContentWrapper-sc-qh4uzj-1 eiUgVo media-content content ml-5"
|
||||
>
|
||||
<h4
|
||||
className="has-text-link"
|
||||
>
|
||||
Test
|
||||
</h4>
|
||||
<p>
|
||||
Test
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`LoginInfo should render extension with login form 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="column is-4 box has-text-centered has-background-secondary-less"
|
||||
>
|
||||
<h3
|
||||
className="title"
|
||||
>
|
||||
login.title
|
||||
</h3>
|
||||
<p
|
||||
className="subtitle"
|
||||
>
|
||||
login.subtitle
|
||||
</p>
|
||||
<div
|
||||
className="LoginInfo__TopMarginBox-sc-dmmy9i-0 hXYZcL box"
|
||||
>
|
||||
<figure
|
||||
className="LoginInfo__AvatarWrapper-sc-dmmy9i-1 ikoLxp"
|
||||
>
|
||||
<img
|
||||
alt="login.logo-alt"
|
||||
className="LoginInfo__AvatarImage-sc-dmmy9i-2 gFcMrK"
|
||||
src="/images/blibSmallLightBackground.svg"
|
||||
/>
|
||||
</figure>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<fieldset
|
||||
className="field"
|
||||
>
|
||||
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<input
|
||||
aria-describedby="input_5"
|
||||
aria-labelledby="input_4"
|
||||
className="input"
|
||||
data-testid="username-input"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder="login.username-placeholder"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
className="field"
|
||||
>
|
||||
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<input
|
||||
aria-describedby="input_7"
|
||||
aria-labelledby="input_6"
|
||||
className="input"
|
||||
data-testid="password-input"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder="login.password-placeholder"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<button
|
||||
className="button is-primary is-fullwidth"
|
||||
data-testid="login-button"
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
|
||||
login.submit
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<button>
|
||||
Login with OAuth2
|
||||
</button>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
className="column is-7 is-offset-1 p-0"
|
||||
>
|
||||
<a
|
||||
className="is-block mb-5"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__InfoBoxWrapper-sc-qh4uzj-2 dgRYgc box media"
|
||||
>
|
||||
<figure
|
||||
className="media-left"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__FixedSizedIconWrapper-sc-qh4uzj-0 bCVWB image box has-text-weight-bold has-text-secondary-least has-background-info is-flex is-flex-direction-column is-justify-content-center is-align-items-center"
|
||||
>
|
||||
<i
|
||||
aria-label=""
|
||||
className="fas fa-fw fa-star has-text-white mb-2 fa-2x"
|
||||
onKeyPress={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.feature
|
||||
</div>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.tip
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div
|
||||
className="InfoBox__ContentWrapper-sc-qh4uzj-1 eiUgVo media-content content ml-5"
|
||||
>
|
||||
<h4
|
||||
className="has-text-link"
|
||||
>
|
||||
Test
|
||||
</h4>
|
||||
<p>
|
||||
Test
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
className="is-block mb-5"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__InfoBoxWrapper-sc-qh4uzj-2 dgRYgc box media"
|
||||
>
|
||||
<figure
|
||||
className="media-left"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__FixedSizedIconWrapper-sc-qh4uzj-0 bCVWB image box has-text-weight-bold has-text-secondary-least has-background-info is-flex is-flex-direction-column is-justify-content-center is-align-items-center"
|
||||
>
|
||||
<i
|
||||
aria-label=""
|
||||
className="fas fa-fw fa-puzzle-piece has-text-white mb-2 fa-2x"
|
||||
onKeyPress={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.plugin
|
||||
</div>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.tip
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div
|
||||
className="InfoBox__ContentWrapper-sc-qh4uzj-1 eiUgVo media-content content ml-5"
|
||||
>
|
||||
<h4
|
||||
className="has-text-link"
|
||||
>
|
||||
Test
|
||||
</h4>
|
||||
<p>
|
||||
Test
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`LoginInfo should render login page 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="column is-4 box has-text-centered has-background-secondary-less"
|
||||
>
|
||||
<h3
|
||||
className="title"
|
||||
>
|
||||
login.title
|
||||
</h3>
|
||||
<p
|
||||
className="subtitle"
|
||||
>
|
||||
login.subtitle
|
||||
</p>
|
||||
<div
|
||||
className="LoginInfo__TopMarginBox-sc-dmmy9i-0 hXYZcL box"
|
||||
>
|
||||
<figure
|
||||
className="LoginInfo__AvatarWrapper-sc-dmmy9i-1 ikoLxp"
|
||||
>
|
||||
<img
|
||||
alt="login.logo-alt"
|
||||
className="LoginInfo__AvatarImage-sc-dmmy9i-2 gFcMrK"
|
||||
src="/images/blibSmallLightBackground.svg"
|
||||
/>
|
||||
</figure>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<fieldset
|
||||
className="field"
|
||||
>
|
||||
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<input
|
||||
aria-describedby="input_1"
|
||||
aria-labelledby="input_0"
|
||||
className="input"
|
||||
data-testid="username-input"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder="login.username-placeholder"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
className="field"
|
||||
>
|
||||
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<input
|
||||
aria-describedby="input_3"
|
||||
aria-labelledby="input_2"
|
||||
className="input"
|
||||
data-testid="password-input"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
placeholder="login.password-placeholder"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<button
|
||||
className="button is-primary is-fullwidth"
|
||||
data-testid="login-button"
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
|
||||
login.submit
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
className="column is-7 is-offset-1 p-0"
|
||||
>
|
||||
<a
|
||||
className="is-block mb-5"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__InfoBoxWrapper-sc-qh4uzj-2 dgRYgc box media"
|
||||
>
|
||||
<figure
|
||||
className="media-left"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__FixedSizedIconWrapper-sc-qh4uzj-0 bCVWB image box has-text-weight-bold has-text-secondary-least has-background-info is-flex is-flex-direction-column is-justify-content-center is-align-items-center"
|
||||
>
|
||||
<i
|
||||
aria-label=""
|
||||
className="fas fa-fw fa-star has-text-white mb-2 fa-2x"
|
||||
onKeyPress={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.feature
|
||||
</div>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.tip
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div
|
||||
className="InfoBox__ContentWrapper-sc-qh4uzj-1 eiUgVo media-content content ml-5"
|
||||
>
|
||||
<h4
|
||||
className="has-text-link"
|
||||
>
|
||||
Test
|
||||
</h4>
|
||||
<p>
|
||||
Test
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
className="is-block mb-5"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__InfoBoxWrapper-sc-qh4uzj-2 dgRYgc box media"
|
||||
>
|
||||
<figure
|
||||
className="media-left"
|
||||
>
|
||||
<div
|
||||
className="InfoBox__FixedSizedIconWrapper-sc-qh4uzj-0 bCVWB image box has-text-weight-bold has-text-secondary-least has-background-info is-flex is-flex-direction-column is-justify-content-center is-align-items-center"
|
||||
>
|
||||
<i
|
||||
aria-label=""
|
||||
className="fas fa-fw fa-puzzle-piece has-text-white mb-2 fa-2x"
|
||||
onKeyPress={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.plugin
|
||||
</div>
|
||||
<div
|
||||
className="is-size-4 has-text-white"
|
||||
>
|
||||
login.tip
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div
|
||||
className="InfoBox__ContentWrapper-sc-qh4uzj-1 eiUgVo media-content content ml-5"
|
||||
>
|
||||
<h4
|
||||
className="has-text-link"
|
||||
>
|
||||
Test
|
||||
</h4>
|
||||
<p>
|
||||
Test
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
@@ -1,47 +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 i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import users from "../public/locales/en/users.json";
|
||||
import commons from "../public/locales/en/commons.json";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
|
||||
// have a common namespace used around the full app
|
||||
ns: ["translationsNS"],
|
||||
defaultNS: "translationsNS",
|
||||
|
||||
debug: true,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react!!
|
||||
},
|
||||
|
||||
resources: { en: { users, commons } },
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -26,11 +26,8 @@ import * as React from "react";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
|
||||
import UserForm from "./UserForm";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import i18nTest from "../../i18n.mock";
|
||||
import { User } from "@scm-manager/ui-types";
|
||||
|
||||
const renderWithI18n = (component) => render(<I18nextProvider i18n={i18nTest}>{component}</I18nextProvider>);
|
||||
import "@scm-manager/ui-tests";
|
||||
|
||||
describe("for user creation", () => {
|
||||
const fillForm = (userId: string, displayName: string, password: string, confirmation: string) => {
|
||||
@@ -54,7 +51,7 @@ describe("for user creation", () => {
|
||||
it("should allow to create user", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm submitForm={mockSubmitForm} />);
|
||||
render(<UserForm submitForm={mockSubmitForm} />);
|
||||
|
||||
fillForm("trillian", "Tricia McMillan", "password", "password");
|
||||
|
||||
@@ -66,7 +63,7 @@ describe("for user creation", () => {
|
||||
it("should prevent to submit empty form", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm submitForm={mockSubmitForm} />);
|
||||
render(<UserForm submitForm={mockSubmitForm} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("submit-button"));
|
||||
|
||||
@@ -76,7 +73,7 @@ describe("for user creation", () => {
|
||||
it("should prevent to submit form without user id", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm submitForm={mockSubmitForm} />);
|
||||
render(<UserForm submitForm={mockSubmitForm} />);
|
||||
|
||||
fillForm("", "Arthur Dent", "password", "password");
|
||||
|
||||
@@ -88,7 +85,7 @@ describe("for user creation", () => {
|
||||
it("should prevent to submit form without display name", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm submitForm={mockSubmitForm} />);
|
||||
render(<UserForm submitForm={mockSubmitForm} />);
|
||||
|
||||
fillForm("trillian", "", "password", "password");
|
||||
|
||||
@@ -100,7 +97,7 @@ describe("for user creation", () => {
|
||||
it("should prevent to submit form without password", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm submitForm={mockSubmitForm} />);
|
||||
render(<UserForm submitForm={mockSubmitForm} />);
|
||||
|
||||
fillForm("trillian", "Tricia McMillan", "", "");
|
||||
|
||||
@@ -112,7 +109,7 @@ describe("for user creation", () => {
|
||||
it("should prevent to submit form with wrong password confirmation", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm submitForm={mockSubmitForm} />);
|
||||
render(<UserForm submitForm={mockSubmitForm} />);
|
||||
|
||||
fillForm("trillian", "Tricia McMillan", "password", "different");
|
||||
|
||||
@@ -136,7 +133,7 @@ describe("for user edit", () => {
|
||||
it("should allow to edit user with changed display name", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId("input-displayname"), {
|
||||
target: { value: "Just Tricia" },
|
||||
@@ -150,7 +147,7 @@ describe("for user edit", () => {
|
||||
it("should allow to edit user with changed email", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId("input-mail"), {
|
||||
target: { value: "tricia@hg2g.com" },
|
||||
@@ -164,7 +161,7 @@ describe("for user edit", () => {
|
||||
it("should allow to edit user with changed active flag", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("checkbox-active"));
|
||||
|
||||
@@ -176,7 +173,7 @@ describe("for user edit", () => {
|
||||
it("should prevent to submit unchanged user", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("submit-button"));
|
||||
|
||||
@@ -186,7 +183,7 @@ describe("for user edit", () => {
|
||||
it("should prevent to edit user with incorrect email", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId("input-mail"), {
|
||||
target: { value: "do_not_reply" },
|
||||
@@ -200,7 +197,7 @@ describe("for user edit", () => {
|
||||
it("should prevent to edit user with empty display name", () => {
|
||||
const mockSubmitForm = jest.fn();
|
||||
|
||||
renderWithI18n(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
render(<UserForm user={user} submitForm={mockSubmitForm} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId("input-displayname"), {
|
||||
target: { value: "" },
|
||||
|
||||
Reference in New Issue
Block a user