-
Notifications
You must be signed in to change notification settings - Fork 702
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Andres Martinez Gotor
committed
Jul 6, 2020
1 parent
7adf777
commit 34149b5
Showing
11 changed files
with
576 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
.login { | ||
&-wrapper { | ||
.login { | ||
min-height: calc(100vh - 3rem); | ||
.login-group { | ||
padding: 0 0 0 0; | ||
} | ||
.error.active { | ||
font-size: small; | ||
} | ||
} | ||
} | ||
&-submit-button { | ||
display: grid; | ||
padding: 1rem 0 0 0; | ||
} | ||
&-moreinfo { | ||
display: grid; | ||
justify-content: end; | ||
margin-top: 0.5rem; | ||
svg { | ||
margin-top: -0.1rem; | ||
} | ||
} | ||
} | ||
|
||
.title h3 span { | ||
font-size: xx-large; | ||
display: block; | ||
} | ||
|
||
#login-submit-button { | ||
display: contents; | ||
} |
159 changes: 159 additions & 0 deletions
159
dashboard/src/components/LoginForm/LoginForm.v2.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import { mount, shallow } from "enzyme"; | ||
import { Location } from "history"; | ||
import context from "jest-plugin-context"; | ||
import * as React from "react"; | ||
import { act } from "react-dom/test-utils"; | ||
import { Redirect } from "react-router"; | ||
import itBehavesLike from "../../shared/specs"; | ||
import LoginForm from "./LoginForm.v2"; | ||
import OAuthLogin from "./OauthLogin"; | ||
import TokenLogin from "./TokenLogin"; | ||
|
||
const emptyLocation: Location = { | ||
hash: "", | ||
pathname: "", | ||
search: "", | ||
state: "", | ||
}; | ||
|
||
const defaultProps = { | ||
authenticate: jest.fn(), | ||
authenticated: false, | ||
authenticating: false, | ||
authenticationError: undefined, | ||
location: emptyLocation, | ||
checkCookieAuthentication: jest.fn(), | ||
oauthLoginURI: "", | ||
appVersion: "devel", | ||
}; | ||
|
||
const authenticationError = "it's a trap"; | ||
|
||
describe("componentDidMount", () => { | ||
it("calls checkCookieAuthentication when oauthLoginURI provided", () => { | ||
const props = { | ||
...defaultProps, | ||
oauthLoginURI: "/sign/in", | ||
}; | ||
const checkCookieAuthentication = jest.fn(); | ||
mount(<LoginForm {...props} checkCookieAuthentication={checkCookieAuthentication} />); | ||
expect(checkCookieAuthentication).toHaveBeenCalled(); | ||
}); | ||
|
||
it("does not call checkCookieAuthentication when oauthLoginURI not provided", () => { | ||
const checkCookieAuthentication = jest.fn(); | ||
mount(<LoginForm {...defaultProps} checkCookieAuthentication={checkCookieAuthentication} />); | ||
expect(checkCookieAuthentication).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
context("while authenticating", () => { | ||
itBehavesLike("aLoadingComponent", { | ||
component: LoginForm, | ||
props: { ...defaultProps, authenticating: true }, | ||
}); | ||
}); | ||
|
||
describe("token login form", () => { | ||
it("renders a token login form", () => { | ||
const wrapper = shallow(<LoginForm {...defaultProps} />); | ||
expect(wrapper.find(TokenLogin)).toExist(); | ||
expect(wrapper.find(OAuthLogin)).not.toExist(); | ||
expect(wrapper).toMatchSnapshot(); | ||
}); | ||
|
||
it("renders a link to the access control documentation", () => { | ||
const wrapper = shallow(<LoginForm {...defaultProps} />); | ||
expect(wrapper.find("a").props()).toMatchObject({ | ||
href: "https://github.com/kubeapps/kubeapps/blob/devel/docs/user/access-control.md", | ||
target: "_blank", | ||
}); | ||
}); | ||
|
||
it("updates the token in the state when the input is changed", () => { | ||
const wrapper = mount(<LoginForm {...defaultProps} />); | ||
let input = wrapper.find("input#token"); | ||
act(() => { | ||
input.simulate("change", { | ||
target: { value: "f00b4r" }, | ||
current: { value: "ff00b4r" }, | ||
}); | ||
}); | ||
wrapper.update(); | ||
input = wrapper.find("input#token"); | ||
expect(input.prop("value")).toBe("f00b4r"); | ||
}); | ||
|
||
describe("redirect if authenticated", () => { | ||
it("redirects to / if no current location", () => { | ||
const wrapper = shallow(<LoginForm {...defaultProps} authenticated={true} />); | ||
const redirect = wrapper.find(Redirect); | ||
expect(redirect.props()).toEqual({ to: { pathname: "/" } }); | ||
}); | ||
|
||
it("redirects to previous location", () => { | ||
const location = Object.assign({}, emptyLocation); | ||
location.state = { from: "/test" }; | ||
const wrapper = shallow( | ||
<LoginForm {...defaultProps} authenticated={true} location={location} />, | ||
); | ||
const redirect = wrapper.find(Redirect); | ||
expect(redirect.props()).toEqual({ to: "/test" }); | ||
}); | ||
}); | ||
|
||
it("calls the authenticate handler when the form is submitted", () => { | ||
const authenticate = jest.fn(); | ||
const wrapper = mount(<LoginForm {...defaultProps} authenticate={authenticate} />); | ||
act(() => { | ||
wrapper.find("input#token").simulate("change", { target: { value: "f00b4r" } }); | ||
}); | ||
act(() => { | ||
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() }); | ||
}); | ||
expect(authenticate).toBeCalledWith("f00b4r"); | ||
}); | ||
|
||
it("displays an error if the authentication error is passed", () => { | ||
const wrapper = mount( | ||
<LoginForm {...defaultProps} authenticationError={authenticationError} />, | ||
); | ||
|
||
expect(wrapper.find(".error").exists()).toBe(true); | ||
expect(wrapper).toMatchSnapshot(); | ||
}); | ||
|
||
it("does not display the oauth login if oauthLoginURI provided", () => { | ||
const props = { | ||
...defaultProps, | ||
oauthLoginURI: "", | ||
}; | ||
|
||
const wrapper = shallow(<LoginForm {...props} />); | ||
|
||
expect(wrapper.find("a.button").exists()).toBe(false); | ||
}); | ||
}); | ||
|
||
describe("oauth login form", () => { | ||
const props = { | ||
...defaultProps, | ||
oauthLoginURI: "/sign/in", | ||
}; | ||
it("does not display the token login if oauthLoginURI provided", () => { | ||
const wrapper = mount(<LoginForm {...props} />); | ||
|
||
expect(wrapper.find("input#token").exists()).toBe(false); | ||
}); | ||
|
||
it("displays the oauth login if oauthLoginURI provided", () => { | ||
const wrapper = mount(<LoginForm {...props} />); | ||
|
||
expect(wrapper.find("a").findWhere(a => a.prop("href") === props.oauthLoginURI)).toExist(); | ||
}); | ||
|
||
it("renders a login button link", () => { | ||
const wrapper = shallow(<LoginForm {...props} />); | ||
expect(wrapper).toMatchSnapshot(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { ClarityIcons, infoCircleIcon } from "@clr/core/icon-shapes"; | ||
import { Location } from "history"; | ||
import * as React from "react"; | ||
import { useEffect, useState } from "react"; | ||
import { Redirect } from "react-router"; | ||
import { CdsIcon } from "../Clarity/clarity"; | ||
|
||
import LoadingWrapper from "../../components/LoadingWrapper"; | ||
import "./LoginForm.v2.css"; | ||
import OAuthLogin from "./OauthLogin"; | ||
import TokenLogin from "./TokenLogin"; | ||
|
||
ClarityIcons.addIcons(infoCircleIcon); | ||
|
||
export interface ILoginFormProps { | ||
authenticated: boolean; | ||
authenticating: boolean; | ||
authenticationError: string | undefined; | ||
oauthLoginURI: string; | ||
authenticate: (token: string) => any; | ||
checkCookieAuthentication: () => void; | ||
appVersion: string; | ||
location: Location; | ||
} | ||
|
||
function LoginForm(props: ILoginFormProps) { | ||
const [token, setToken] = useState(""); | ||
const { oauthLoginURI, checkCookieAuthentication } = props; | ||
useEffect(() => { | ||
if (oauthLoginURI) { | ||
checkCookieAuthentication(); | ||
} | ||
}, [oauthLoginURI, checkCookieAuthentication]); | ||
|
||
if (props.authenticating) { | ||
return <LoadingWrapper />; | ||
} | ||
if (props.authenticated) { | ||
const { from } = (props.location.state as any) || { from: { pathname: "/" } }; | ||
return <Redirect to={from} />; | ||
} | ||
|
||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||
e.preventDefault(); | ||
return token && (await props.authenticate(token)); | ||
}; | ||
|
||
const handleTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
setToken(e.target.value); | ||
}; | ||
|
||
return ( | ||
<div className="login-wrapper"> | ||
<form className="login clr-form" onSubmit={handleSubmit}> | ||
{props.oauthLoginURI ? ( | ||
<OAuthLogin | ||
authenticationError={props.authenticationError} | ||
oauthLoginURI={props.oauthLoginURI} | ||
/> | ||
) : ( | ||
<TokenLogin | ||
authenticationError={props.authenticationError} | ||
token={token} | ||
handleTokenChange={handleTokenChange} | ||
/> | ||
)} | ||
<div className="login-moreinfo"> | ||
<a | ||
href={`https://github.com/kubeapps/kubeapps/blob/${props.appVersion}/docs/user/access-control.md`} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<CdsIcon shape="info-circle" /> | ||
More Info | ||
</a> | ||
</div> | ||
</form> | ||
</div> | ||
); | ||
} | ||
|
||
export default LoginForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import * as React from "react"; | ||
import { CdsButton } from "../Clarity/clarity"; | ||
|
||
interface ILoginFormProps { | ||
authenticationError: string | undefined; | ||
oauthLoginURI: string; | ||
} | ||
|
||
function OAuthLogin(props: ILoginFormProps) { | ||
return ( | ||
<section className="title" aria-labelledby="login-title" aria-describedby="login-desc"> | ||
<h3 id="login-title" className="welcome"> | ||
Welcome to <span>Kubeapps</span> | ||
</h3> | ||
<p id="login-desc" className="hint"> | ||
Your cluster operator has enabled login via an authentication provider. | ||
</p> | ||
<div className="login-group"> | ||
{props.authenticationError && ( | ||
<div className="error active"> | ||
There was an error connecting to the Kubernetes API. Please check that your token is | ||
valid. | ||
</div> | ||
)} | ||
<a href={props.oauthLoginURI} className="login-submit-button"> | ||
<CdsButton id="login-submit-button" status="primary"> | ||
Login via OIDC Provider | ||
</CdsButton> | ||
</a> | ||
</div> | ||
</section> | ||
); | ||
} | ||
|
||
export default OAuthLogin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import * as React from "react"; | ||
import { CdsButton } from "../Clarity/clarity"; | ||
|
||
interface ILoginFormProps { | ||
token: string; | ||
authenticationError: string | undefined; | ||
handleTokenChange: (e: React.ChangeEvent<HTMLInputElement>) => void; | ||
} | ||
|
||
function TokenLogin(props: ILoginFormProps) { | ||
return ( | ||
<section className="title" aria-labelledby="login-title" aria-describedby="login-desc"> | ||
<h3 id="login-title" className="welcome"> | ||
Welcome to <span>Kubeapps</span> | ||
</h3> | ||
<p id="login-desc" className="hint"> | ||
Your cluster operator should provide you with a Kubernetes API token. | ||
</p> | ||
<div className="login-group"> | ||
<div className="clr-form-control"> | ||
<label htmlFor="token" className="clr-control-label"> | ||
Token | ||
</label> | ||
<div className="clr-control-container"> | ||
<div className="clr-input-wrapper"> | ||
<input | ||
type="password" | ||
id="token" | ||
placeholder="Paste token here" | ||
className="clr-input" | ||
required={true} | ||
onChange={props.handleTokenChange} | ||
value={props.token} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
{props.authenticationError && ( | ||
<div className="error active "> | ||
There was an error connecting to the Kubernetes API. Please check that your token is | ||
valid. | ||
</div> | ||
)} | ||
<div className="login-submit-button"> | ||
<CdsButton id="login-submit-button" status="primary"> | ||
Submit | ||
</CdsButton> | ||
</div> | ||
</div> | ||
</section> | ||
); | ||
} | ||
|
||
export default TokenLogin; |
Oops, something went wrong.