K7 Login screen (#23512)
This PR implements the K7 Login screen, as described in #20015 @ryankeairns @snide @kobelb ## Login form <img width="1310" alt="login" src="https://user-images.githubusercontent.com/3493255/46048123-d9e53d80-c0f5-11e8-9e56-acbe3a8f2b5a.png"> ## Invalid credentials ![image](https://user-images.githubusercontent.com/3493255/46147388-621d2d00-c233-11e8-8a75-beba7bb5a2b2.png) ## Session expired ![image](https://user-images.githubusercontent.com/3493255/46147408-6fd2b280-c233-11e8-97c1-8acb7b2e0c06.png) ## No connection to Elasticsearch ![image](https://user-images.githubusercontent.com/3493255/46147515-adcfd680-c233-11e8-8ad4-3a21d2c06b2c.png) ## Insecure connection ![image](https://user-images.githubusercontent.com/3493255/46147433-8416af80-c233-11e8-82dc-6f32a5ec1bd0.png) Closes #20015
This commit is contained in:
parent
5d64140953
commit
7d8ce7bc01
|
@ -18,24 +18,13 @@
|
|||
*/
|
||||
|
||||
export function ShieldPageProvider({ getService }) {
|
||||
const remote = getService('remote');
|
||||
const config = getService('config');
|
||||
|
||||
const defaultFindTimeout = config.get('timeouts.find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
class ShieldPage {
|
||||
login(user, pwd) {
|
||||
return remote.setFindTimeout(defaultFindTimeout)
|
||||
.findById('username')
|
||||
.type(user)
|
||||
.then(function () {
|
||||
return remote.findById('password')
|
||||
.type(pwd);
|
||||
})
|
||||
.then(function () {
|
||||
return remote.findByCssSelector('button')
|
||||
.click();
|
||||
});
|
||||
async login(user, pwd) {
|
||||
await testSubjects.setValue('loginUsername', user);
|
||||
await testSubjects.setValue('loginPassword', pwd);
|
||||
await testSubjects.click('loginSubmit');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
13
x-pack/plugins/security/common/login_state.ts
Normal file
13
x-pack/plugins/security/common/login_state.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type LoginLayout = 'form' | 'error-es-unavailable' | 'error-xpack-unavailable';
|
||||
|
||||
export interface LoginState {
|
||||
layout: LoginLayout;
|
||||
allowLogin: boolean;
|
||||
loginMessage: string;
|
||||
}
|
|
@ -56,6 +56,7 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
uiExports: {
|
||||
chromeNavControls: ['plugins/security/views/nav_control'],
|
||||
managementSections: ['plugins/security/views/management'],
|
||||
styleSheetPaths: `${__dirname}/public/index.scss`,
|
||||
apps: [{
|
||||
id: 'login',
|
||||
title: 'Login',
|
||||
|
|
66
x-pack/plugins/security/public/assets/bg_bottom_branded.svg
Normal file
66
x-pack/plugins/security/public/assets/bg_bottom_branded.svg
Normal file
|
@ -0,0 +1,66 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1169" height="880" viewBox="0 0 1169 880">
|
||||
<defs>
|
||||
<radialGradient id="bg_bottom_branded-b" cx="44.645%" cy="43.259%" r="93.821%" fx="44.645%" fy="43.259%" gradientTransform="matrix(.54075 .76504 -.6424 .64398 .483 -.188)">
|
||||
<stop offset="0%" stop-color="#D9D9D9"/>
|
||||
<stop offset="100%"/>
|
||||
</radialGradient>
|
||||
<polygon id="bg_bottom_branded-a" points="0 0 1048 880 0 880"/>
|
||||
<linearGradient id="bg_bottom_branded-c" x1="98.924%" x2="7.157%" y1="48.924%" y2="48.924%">
|
||||
<stop offset="0%" stop-color="#DFDDDD" stop-opacity=".25"/>
|
||||
<stop offset="100%" stop-color="#FFF" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bg_bottom_branded-e" x1="0%" y1="47.421%" y2="47.421%">
|
||||
<stop offset="0%" stop-color="#FFF" stop-opacity=".6"/>
|
||||
<stop offset="100%" stop-opacity=".25"/>
|
||||
</linearGradient>
|
||||
<polygon id="bg_bottom_branded-d" points="560 364 1169 880 560 880"/>
|
||||
<linearGradient id="bg_bottom_branded-g" x1="0%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFF" stop-opacity=".2"/>
|
||||
<stop offset="100%" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="bg_bottom_branded-h" cx="0%" cy="0%" r="127.62%" fx="0%" fy="0%" gradientTransform="scale(.9322 1) rotate(42.99)">
|
||||
<stop offset="0%" stop-color="#BBB" stop-opacity=".1"/>
|
||||
<stop offset="100%" stop-opacity=".5"/>
|
||||
</radialGradient>
|
||||
<polygon id="bg_bottom_branded-f" points="-12 538 342 868 -12 868"/>
|
||||
<linearGradient id="bg_bottom_branded-j" x1="0%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFF" stop-opacity=".4"/>
|
||||
<stop offset="100%" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="bg_bottom_branded-k" cx="0%" cy="0%" r="148.368%" fx="0%" fy="0%" gradientTransform="matrix(.674 .674 -.73874 .61493 0 0)">
|
||||
<stop offset="0%" stop-color="#FFF" stop-opacity=".101"/>
|
||||
<stop offset="100%" stop-opacity=".15"/>
|
||||
</radialGradient>
|
||||
<path id="bg_bottom_branded-i" d="M197,880 L374,880 C365.385564,795.927984 296.005151,723.868711 197,686 L197,880 Z"/>
|
||||
<radialGradient id="bg_bottom_branded-m" cx="0%" cy="0%" r="127.62%" fx="0%" fy="0%" gradientTransform="scale(1 .9322) rotate(47.01)">
|
||||
<stop offset="0%" stop-color="#BBB" stop-opacity=".1"/>
|
||||
<stop offset="100%" stop-opacity=".5"/>
|
||||
</radialGradient>
|
||||
<polygon id="bg_bottom_branded-l" points="165 703 330 880 165 880"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g opacity=".1">
|
||||
<use fill="#E4E4E4" xlink:href="#bg_bottom_branded-a"/>
|
||||
<use fill="url(#bg_bottom_branded-b)" xlink:href="#bg_bottom_branded-a" style="mix-blend-mode:overlay"/>
|
||||
</g>
|
||||
<g opacity=".05">
|
||||
<use fill="url(#bg_bottom_branded-c)" xlink:href="#bg_bottom_branded-d"/>
|
||||
<use fill="url(#bg_bottom_branded-e)" xlink:href="#bg_bottom_branded-d" style="mix-blend-mode:overlay"/>
|
||||
</g>
|
||||
<g opacity=".65" transform="matrix(0 -1 -1 0 868 868)">
|
||||
<use fill="#DD0A73" xlink:href="#bg_bottom_branded-f"/>
|
||||
<use fill="url(#bg_bottom_branded-g)" xlink:href="#bg_bottom_branded-f" style="mix-blend-mode:overlay"/>
|
||||
<use fill="url(#bg_bottom_branded-h)" xlink:href="#bg_bottom_branded-f" style="mix-blend-mode:overlay"/>
|
||||
</g>
|
||||
<g opacity=".65">
|
||||
<use fill="#017F75" xlink:href="#bg_bottom_branded-i"/>
|
||||
<use fill="url(#bg_bottom_branded-j)" xlink:href="#bg_bottom_branded-i" style="mix-blend-mode:overlay"/>
|
||||
<use fill="url(#bg_bottom_branded-k)" xlink:href="#bg_bottom_branded-i" style="mix-blend-mode:overlay"/>
|
||||
</g>
|
||||
<g opacity=".15">
|
||||
<use fill="#353535" xlink:href="#bg_bottom_branded-l"/>
|
||||
<use fill="url(#bg_bottom_branded-g)" xlink:href="#bg_bottom_branded-l" style="mix-blend-mode:overlay"/>
|
||||
<use fill="url(#bg_bottom_branded-m)" xlink:href="#bg_bottom_branded-l" style="mix-blend-mode:overlay"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
33
x-pack/plugins/security/public/assets/bg_top_branded.svg
Normal file
33
x-pack/plugins/security/public/assets/bg_top_branded.svg
Normal file
|
@ -0,0 +1,33 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="373" viewBox="0 0 400 373">
|
||||
<defs>
|
||||
<linearGradient id="bg_top_branded-a" x1="10.793%" y1="50%" y2="50%">
|
||||
<stop offset="0%" stop-color="#FFF" stop-opacity=".5"/>
|
||||
<stop offset="100%" stop-color="#E2E2E2" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="bg_top_branded-c" cx="0%" cy="0%" r="146.629%" fx="0%" fy="0%" gradientTransform="matrix(.68199 .682 -.63596 .73135 0 0)">
|
||||
<stop offset="0%" stop-color="#FFF" stop-opacity=".5"/>
|
||||
<stop offset="100%" stop-opacity=".2"/>
|
||||
</radialGradient>
|
||||
<polygon id="bg_top_branded-b" points="400 373 0 0 400 0"/>
|
||||
<linearGradient id="bg_top_branded-e" x1="0%" y1="35.11%" y2="0%">
|
||||
<stop offset="0%" stop-color="#FFF" stop-opacity=".6"/>
|
||||
<stop offset="100%" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="bg_top_branded-f" cy="0%" r="146.096%" fx="50%" fy="0%" gradientTransform="matrix(-.68448 .68448 -.64265 -.72903 .842 -.342)">
|
||||
<stop offset="0%" stop-color="#FFF" stop-opacity=".7"/>
|
||||
<stop offset="100%"/>
|
||||
</radialGradient>
|
||||
<polygon id="bg_top_branded-d" points="400 169 220 0 400 0"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g opacity=".1">
|
||||
<use fill="url(#bg_top_branded-a)" xlink:href="#bg_top_branded-b"/>
|
||||
<use fill="url(#bg_top_branded-c)" xlink:href="#bg_top_branded-b" style="mix-blend-mode:overlay"/>
|
||||
</g>
|
||||
<g opacity=".05">
|
||||
<use fill="#F5F5F5" xlink:href="#bg_top_branded-d"/>
|
||||
<use fill="url(#bg_top_branded-e)" xlink:href="#bg_top_branded-d" style="mix-blend-mode:overlay"/>
|
||||
<use fill="url(#bg_top_branded-f)" xlink:href="#bg_top_branded-d" style="mix-blend-mode:darken"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
4
x-pack/plugins/security/public/index.scss
Normal file
4
x-pack/plugins/security/public/index.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
@import 'ui/public/styles/styling_constants';
|
||||
|
||||
// Login styles
|
||||
@import './views/login/index';
|
|
@ -6,16 +6,23 @@
|
|||
|
||||
import { parse } from 'url';
|
||||
|
||||
export function parseNext(href, basePath = '') {
|
||||
export function parseNext(href: string, basePath = '') {
|
||||
const { query, hash } = parse(href, true);
|
||||
if (!query.next) {
|
||||
return `${basePath}/`;
|
||||
}
|
||||
|
||||
let next: string;
|
||||
if (Array.isArray(query.next) && query.next.length > 0) {
|
||||
next = query.next[0];
|
||||
} else {
|
||||
next = query.next as string;
|
||||
}
|
||||
|
||||
// validate that `next` is not attempting a redirect to somewhere
|
||||
// outside of this Kibana install
|
||||
const { protocol, hostname, port, pathname } = parse(
|
||||
query.next,
|
||||
next,
|
||||
false /* parseQueryString */,
|
||||
true /* slashesDenoteHost */
|
||||
);
|
9
x-pack/plugins/security/public/views/login/_index.scss
Normal file
9
x-pack/plugins/security/public/views/login/_index.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Prefix all styles with "login" to avoid conflicts.
|
||||
// Examples
|
||||
// loginChart
|
||||
// loginChart__legend
|
||||
// loginChart__legend--small
|
||||
// loginChart__legend-isLoading
|
||||
|
||||
@import 'login';
|
||||
|
74
x-pack/plugins/security/public/views/login/_login.scss
Normal file
74
x-pack/plugins/security/public/views/login/_login.scss
Normal file
|
@ -0,0 +1,74 @@
|
|||
|
||||
.loginWelcome {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $euiZLevel9 + 1000;
|
||||
background: inherit;
|
||||
background-image: linear-gradient(0deg, $euiColorLightestShade 0%, $euiColorEmptyShade 100%);
|
||||
opacity: 0;
|
||||
overflow: auto;
|
||||
animation: loginWelcome_FadeIn $euiAnimSpeedExtraSlow $euiAnimSlightResistance 0s forwards;
|
||||
}
|
||||
|
||||
.loginWelcome::before {
|
||||
// SASSTODO: webpack pipeline isn't setup to handle image urls in SASS yet
|
||||
// content: url(../../assets/bg_top_branded.svg);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loginWelcome::after {
|
||||
// SASSTODO: webpack pipeline isn't setup to handle image urls in SASS yet
|
||||
// content: url(../../assets/bg_bottom_branded.svg);
|
||||
position: fixed;
|
||||
bottom: -2px; // Hides an odd space at the bottom of the svg
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loginWelcome__header {
|
||||
position: relative;
|
||||
padding: $euiSizeXL;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loginWelcome__logo {
|
||||
margin-bottom: $euiSizeXL;
|
||||
@include kibanaCircleLogo;
|
||||
@include euiBottomShadowMedium;
|
||||
}
|
||||
|
||||
.loginWelcome__footerAction {
|
||||
margin-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.loginWelcome__content {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
max-width: 460px;
|
||||
padding-left: $euiSizeXL;
|
||||
padding-right: $euiSizeXL;
|
||||
z-index: 10;
|
||||
|
||||
&.loginWelcome__contentDisabledForm {
|
||||
max-width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes loginWelcome_FadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(200px), scale(0.75);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0), scale(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BasicLoginForm renders as expected 1`] = `
|
||||
<React.Fragment>
|
||||
<EuiPanel
|
||||
grow={true}
|
||||
hasShadow={false}
|
||||
paddingSize="m"
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Username"
|
||||
>
|
||||
<EuiFieldText
|
||||
aria-required={true}
|
||||
compressed={false}
|
||||
data-test-subj="loginUsername"
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
id="username"
|
||||
inputRef={[Function]}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
name="username"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Password"
|
||||
>
|
||||
<EuiFieldText
|
||||
aria-required={true}
|
||||
compressed={false}
|
||||
data-test-subj="loginPassword"
|
||||
disabled={false}
|
||||
fullWidth={false}
|
||||
id="password"
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="loginSubmit"
|
||||
fill={true}
|
||||
iconSide="left"
|
||||
isDisabled={true}
|
||||
isLoading={false}
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Log in
|
||||
</EuiButton>
|
||||
</form>
|
||||
</EuiPanel>
|
||||
</React.Fragment>
|
||||
`;
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiCallOut } from '@elastic/eui';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { LoginState } from '../../../../../common/login_state';
|
||||
import { BasicLoginForm } from './basic_login_form';
|
||||
|
||||
const createMockHttp = ({ simulateError = false } = {}) => {
|
||||
return {
|
||||
post: jest.fn(async () => {
|
||||
if (simulateError) {
|
||||
throw {
|
||||
data: {
|
||||
statusCode: 401,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const createLoginState = (options?: Partial<LoginState>) => {
|
||||
return {
|
||||
allowLogin: true,
|
||||
layout: 'form',
|
||||
loginMessage: '',
|
||||
...options,
|
||||
} as LoginState;
|
||||
};
|
||||
|
||||
describe('BasicLoginForm', () => {
|
||||
it('renders as expected', () => {
|
||||
const mockHttp = createMockHttp();
|
||||
const mockWindow = {};
|
||||
const loginState = createLoginState();
|
||||
expect(
|
||||
shallow(
|
||||
<BasicLoginForm http={mockHttp} window={mockWindow} loginState={loginState} next={''} />
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders an info message when provided', () => {
|
||||
const mockHttp = createMockHttp();
|
||||
const mockWindow = {};
|
||||
const loginState = createLoginState();
|
||||
|
||||
const wrapper = shallow(
|
||||
<BasicLoginForm
|
||||
http={mockHttp}
|
||||
window={mockWindow}
|
||||
loginState={loginState}
|
||||
next={''}
|
||||
infoMessage={'Hey this is an info message'}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiCallOut).props().title).toEqual('Hey this is an info message');
|
||||
});
|
||||
|
||||
it('renders an invalid credentials message', async () => {
|
||||
const mockHttp = createMockHttp({ simulateError: true });
|
||||
const mockWindow = {};
|
||||
const loginState = createLoginState();
|
||||
|
||||
const wrapper = mount(
|
||||
<BasicLoginForm http={mockHttp} window={mockWindow} loginState={loginState} next={''} />
|
||||
);
|
||||
|
||||
wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username' } });
|
||||
wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password' } });
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
|
||||
// Wait for ajax + rerender
|
||||
await Promise.resolve();
|
||||
wrapper.update();
|
||||
await Promise.resolve();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(EuiCallOut).props().title).toEqual(
|
||||
`Invalid username or password. Please try again.`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiCallOut, EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import React, { ChangeEvent, Component, Fragment } from 'react';
|
||||
import { LoginState } from '../../../../../common/login_state';
|
||||
|
||||
interface Props {
|
||||
http: any;
|
||||
window: any;
|
||||
infoMessage?: string;
|
||||
loginState: LoginState;
|
||||
next: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
isLoading: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class BasicLoginForm extends Component<Props, State> {
|
||||
public state = {
|
||||
hasError: false,
|
||||
isLoading: false,
|
||||
username: '',
|
||||
password: '',
|
||||
message: '',
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Fragment>
|
||||
{this.renderMessage()}
|
||||
<EuiPanel>
|
||||
<form onSubmit={this.submit}>
|
||||
<EuiFormRow label="Username">
|
||||
<EuiFieldText
|
||||
id="username"
|
||||
name="username"
|
||||
data-test-subj="loginUsername"
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChange}
|
||||
disabled={this.state.isLoading}
|
||||
isInvalid={false}
|
||||
aria-required
|
||||
inputRef={this.setUsernameInputRef}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow label="Password">
|
||||
<EuiFieldText
|
||||
id="password"
|
||||
name="password"
|
||||
data-test-subj="loginPassword"
|
||||
type="password"
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChange}
|
||||
disabled={this.state.isLoading}
|
||||
isInvalid={false}
|
||||
aria-required
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiButton
|
||||
fill
|
||||
type="submit"
|
||||
color="primary"
|
||||
onClick={this.submit}
|
||||
isLoading={this.state.isLoading}
|
||||
isDisabled={!this.isFormValid()}
|
||||
data-test-subj="loginSubmit"
|
||||
>
|
||||
Log in
|
||||
</EuiButton>
|
||||
</form>
|
||||
</EuiPanel>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMessage = () => {
|
||||
if (this.state.message) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="danger"
|
||||
data-test-subj="loginErrorMessage"
|
||||
title={this.state.message}
|
||||
role="alert"
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.infoMessage) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="primary"
|
||||
data-test-subj="loginInfoMessage"
|
||||
title={this.props.infoMessage}
|
||||
role="status"
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
private setUsernameInputRef(ref: HTMLInputElement) {
|
||||
if (ref) {
|
||||
ref.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private isFormValid = () => {
|
||||
const { username, password } = this.state;
|
||||
|
||||
return username && password;
|
||||
};
|
||||
|
||||
private onUsernameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
username: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
password: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private submit = () => {
|
||||
if (!this.isFormValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
message: '',
|
||||
});
|
||||
|
||||
const { http, window, next } = this.props;
|
||||
|
||||
const { username, password } = this.state;
|
||||
|
||||
http.post('./api/security/v1/login', { username, password }).then(
|
||||
() => (window.location.href = next),
|
||||
(error: any) => {
|
||||
const { statusCode = 500 } = error.data || {};
|
||||
|
||||
let message = 'Oops! Error. Try again.';
|
||||
if (statusCode === 401) {
|
||||
message = 'Invalid username or password. Please try again.';
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hasError: true,
|
||||
message,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { BasicLoginForm } from './basic_login_form';
|
|
@ -0,0 +1,35 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DisabledLoginForm renders as expected 1`] = `
|
||||
<EuiPanel
|
||||
grow={true}
|
||||
hasShadow={false}
|
||||
paddingSize="m"
|
||||
>
|
||||
<EuiText
|
||||
color="danger"
|
||||
grow={true}
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<p>
|
||||
disabled message title
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiText
|
||||
grow={true}
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<p>
|
||||
disabled message
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
`;
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { DisabledLoginForm } from './disabled_login_form';
|
||||
|
||||
describe('DisabledLoginForm', () => {
|
||||
it('renders as expected', () => {
|
||||
expect(
|
||||
shallow(<DisabledLoginForm title={'disabled message title'} message={'disabled message'} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiPanel, EuiText } from '@elastic/eui';
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
title: ReactNode;
|
||||
message: ReactNode;
|
||||
}
|
||||
|
||||
export class DisabledLoginForm extends Component<Props, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiText color="danger" style={{ textAlign: 'center' }}>
|
||||
<p>{this.props.title}</p>
|
||||
</EuiText>
|
||||
<EuiText style={{ textAlign: 'center' }}>
|
||||
<p>{this.props.message}</p>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { DisabledLoginForm } from './disabled_login_form';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { LoginPage } from './login_page';
|
|
@ -0,0 +1,463 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when a connection to ES is not available 1`] = `
|
||||
<I18nProvider>
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="kbn.login.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
grow={true}
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="kbn.login.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="See the Kibana logs for details and try reloading the page."
|
||||
id="kbn.login.esUnavailableMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cannot connect to the Elastiscearch cluster"
|
||||
id="kbn.login.esUnavailableTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when an unknown loginState layout is provided 1`] = `
|
||||
<I18nProvider>
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="kbn.login.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
grow={true}
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="kbn.login.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="Refer to the Kibana logs for more details and refresh to try again."
|
||||
id="kbn.login.unknownLayoutMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Unsupported login form layout."
|
||||
id="kbn.login.unknownLayoutTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when secure cookies are required but not present 1`] = `
|
||||
<I18nProvider>
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="kbn.login.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
grow={true}
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="kbn.login.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="Contact your system administrator."
|
||||
id="kbn.login.requiresSecureConnectionMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="A secure connection is required for log in"
|
||||
id="kbn.login.requiresSecureConnectionTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
`;
|
||||
|
||||
exports[`LoginPage disabled form states renders as expected when xpack is not available 1`] = `
|
||||
<I18nProvider>
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="kbn.login.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
grow={true}
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="kbn.login.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body loginWelcome__contentDisabledForm"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<DisabledLoginForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
defaultMessage="To use the full set of free features in this distribution of Kibana, please update Elasticsearch to the default distribution."
|
||||
id="kbn.login.xpackUnavailableMessage"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cannot connect to the Elasticsearch cluster currently configured for Kibana."
|
||||
id="kbn.login.xpackUnavailableTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
`;
|
||||
|
||||
exports[`LoginPage enabled form state renders as expected 1`] = `
|
||||
<I18nProvider>
|
||||
<div
|
||||
className="loginWelcome login-form"
|
||||
>
|
||||
<header
|
||||
className="loginWelcome__header"
|
||||
>
|
||||
<div
|
||||
className="loginWelcome__content eui-textCenter"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<span
|
||||
className="loginWelcome__logo"
|
||||
>
|
||||
<EuiIcon
|
||||
size="xxl"
|
||||
type="logoKibana"
|
||||
/>
|
||||
</span>
|
||||
<EuiTitle
|
||||
className="loginWelcome__title"
|
||||
size="l"
|
||||
>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Kibana"
|
||||
id="kbn.login.welcomeTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="loginWelcome__subtitle"
|
||||
color="subdued"
|
||||
grow={true}
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
id="kbn.login.welcomeDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="loginWelcome__content loginWelcome-body"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<BasicLoginForm
|
||||
http={
|
||||
Object {
|
||||
"post": [MockFunction],
|
||||
}
|
||||
}
|
||||
isSecureConnection={false}
|
||||
loginState={
|
||||
Object {
|
||||
"allowLogin": true,
|
||||
"layout": "form",
|
||||
"loginMessage": "",
|
||||
}
|
||||
}
|
||||
next=""
|
||||
requiresSecureConnection={false}
|
||||
window={Object {}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
`;
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { LoginPage } from './login_page';
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { LoginLayout, LoginState } from '../../../../../common/login_state';
|
||||
import { LoginPage } from './login_page';
|
||||
|
||||
const createMockHttp = ({ simulateError = false } = {}) => {
|
||||
return {
|
||||
post: jest.fn(async () => {
|
||||
if (simulateError) {
|
||||
throw {
|
||||
data: {
|
||||
statusCode: 401,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const createLoginState = (options?: Partial<LoginState>) => {
|
||||
return {
|
||||
allowLogin: true,
|
||||
layout: 'form',
|
||||
loginMessage: '',
|
||||
...options,
|
||||
} as LoginState;
|
||||
};
|
||||
|
||||
describe('LoginPage', () => {
|
||||
describe('disabled form states', () => {
|
||||
it('renders as expected when secure cookies are required but not present', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState(),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: true,
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when a connection to ES is not available', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState({
|
||||
layout: 'error-es-unavailable',
|
||||
}),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when xpack is not available', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState({
|
||||
layout: 'error-xpack-unavailable',
|
||||
}),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders as expected when an unknown loginState layout is provided', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState({
|
||||
layout: 'error-asdf-asdf-unknown' as LoginLayout,
|
||||
}),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabled form state', () => {
|
||||
it('renders as expected', () => {
|
||||
const props = {
|
||||
http: createMockHttp(),
|
||||
window: {},
|
||||
next: '',
|
||||
loginState: createLoginState(),
|
||||
isSecureConnection: false,
|
||||
requiresSecureConnection: false,
|
||||
};
|
||||
|
||||
expect(shallow(<LoginPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// @ts-ignore
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
// @ts-ignore
|
||||
EuiCard,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import { LoginState } from '../../../../../common/login_state';
|
||||
import { BasicLoginForm } from '../basic_login_form';
|
||||
import { DisabledLoginForm } from '../disabled_login_form';
|
||||
|
||||
interface Props {
|
||||
http: any;
|
||||
window: any;
|
||||
next: string;
|
||||
infoMessage?: string;
|
||||
loginState: LoginState;
|
||||
isSecureConnection: boolean;
|
||||
requiresSecureConnection: boolean;
|
||||
}
|
||||
|
||||
export class LoginPage extends Component<Props, {}> {
|
||||
public render() {
|
||||
const allowLogin = this.allowLogin();
|
||||
|
||||
const contentHeaderClasses = classNames('loginWelcome__content', 'eui-textCenter', {
|
||||
['loginWelcome__contentDisabledForm']: !allowLogin,
|
||||
});
|
||||
|
||||
const contentBodyClasses = classNames('loginWelcome__content', 'loginWelcome-body', {
|
||||
['loginWelcome__contentDisabledForm']: !allowLogin,
|
||||
});
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<div className="loginWelcome login-form">
|
||||
<header className="loginWelcome__header">
|
||||
<div className={contentHeaderClasses}>
|
||||
<EuiSpacer size="xxl" />
|
||||
<span className="loginWelcome__logo">
|
||||
<EuiIcon type="logoKibana" size="xxl" />
|
||||
</span>
|
||||
<EuiTitle size="l" className="loginWelcome__title">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="kbn.login.welcomeTitle"
|
||||
defaultMessage="Welcome to Kibana"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText size="s" color="subdued" className="loginWelcome__subtitle">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="kbn.login.welcomeDescription"
|
||||
defaultMessage="Your window into the Elastic Stack"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xl" />
|
||||
</div>
|
||||
</header>
|
||||
<div className={contentBodyClasses}>
|
||||
<EuiFlexGroup gutterSize="l">
|
||||
<EuiFlexItem>{this.getLoginForm()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</div>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private allowLogin = () => {
|
||||
if (this.props.requiresSecureConnection && !this.props.isSecureConnection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.props.loginState.allowLogin && this.props.loginState.layout === 'form';
|
||||
};
|
||||
|
||||
private getLoginForm = () => {
|
||||
if (this.props.requiresSecureConnection && !this.props.isSecureConnection) {
|
||||
return (
|
||||
<DisabledLoginForm
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="kbn.login.requiresSecureConnectionTitle"
|
||||
defaultMessage="A secure connection is required for log in"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="kbn.login.requiresSecureConnectionMessage"
|
||||
defaultMessage="Contact your system administrator."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const layout = this.props.loginState.layout;
|
||||
switch (layout) {
|
||||
case 'form':
|
||||
return <BasicLoginForm {...this.props} />;
|
||||
case 'error-es-unavailable':
|
||||
return (
|
||||
<DisabledLoginForm
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="kbn.login.esUnavailableTitle"
|
||||
defaultMessage="Cannot connect to the Elastiscearch cluster"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="kbn.login.esUnavailableMessage"
|
||||
defaultMessage="See the Kibana logs for details and try reloading the page."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'error-xpack-unavailable':
|
||||
return (
|
||||
<DisabledLoginForm
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="kbn.login.xpackUnavailableTitle"
|
||||
defaultMessage="Cannot connect to the Elasticsearch cluster currently configured for Kibana."
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="kbn.login.xpackUnavailableMessage"
|
||||
defaultMessage="To use the full set of free features in this distribution of Kibana, please update Elasticsearch to the default distribution."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DisabledLoginForm
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="kbn.login.unknownLayoutTitle"
|
||||
defaultMessage="Unsupported login form layout."
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="kbn.login.unknownLayoutMessage"
|
||||
defaultMessage="Refer to the Kibana logs for more details and refresh to try again."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,58 +1 @@
|
|||
<div class="container" ng-class="{error: !!login.error}">
|
||||
<div class="logo-container">
|
||||
<div class="kibanaWelcomeLogo"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-container" ng-if="login.layout === 'form'">
|
||||
<form class="login-form" ng-submit="login.submit(username, password)">
|
||||
<div ng-show="login.error" class="form-group error-message">
|
||||
<label class="control-label" data-test-subj="loginErrorMessage" >Oops! Error. Try again.</label>
|
||||
</div>
|
||||
|
||||
<div ng-if="login.infoMessage" class="form-group error-message">
|
||||
<label class="control-label">{{login.infoMessage}}</label>
|
||||
</div>
|
||||
|
||||
<div ng-if="!login.allowLogin" class="form-group error-message">
|
||||
<label class="control-label">{{login.loginMessage}}</label>
|
||||
</div>
|
||||
|
||||
<div ng-if="login.isDisabled" class="form-group error-message">
|
||||
<label class="control-label">Logging in requires a secure connection. Please contact your administrator.</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group inner-addon left-addon">
|
||||
<i class="fa fa-user fa-lg fa-fw"></i>
|
||||
<input type="text" ng-disabled="login.isDisabled || !login.allowLogin" ng-model="username" class="form-control" id="username" placeholder="Username" autofocus data-test-subj="loginUsername" />
|
||||
</div>
|
||||
|
||||
<div class="form-group inner-addon left-addon">
|
||||
<i class="fa fa-lock fa-lg fa-fw"></i>
|
||||
<input type="password" ng-disabled="login.isDisabled|| !login.allowLogin" ng-model="password" class="form-control" id="password" placeholder="Password" data-test-subj="loginPassword"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button
|
||||
type="submit"
|
||||
ng-disabled="login.isDisabled || !login.allowLogin || !username || !password || login.isLoading"
|
||||
class="kuiButton kuiButton--primary kuiButton--fullWidth"
|
||||
data-test-subj="loginSubmit"
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="euiText loginErrorEsUnavailable" ng-if="login.layout === 'error-es-unavailable'">
|
||||
<p class="euiTitle euiTitle--medium euiTextColor euiTextColor--danger">Cannot connect to the Elasticsearch cluster currently configured for Kibana.</p>
|
||||
<p>Refer to the Kibana logs for more details and refresh to try again.</p>
|
||||
</div>
|
||||
|
||||
<div class="euiText loginErrorXpackUnavailable" ng-if="login.layout === 'error-xpack-unavailable'">
|
||||
<p class="euiTitle euiTitle--small">It appears you're running the oss-only distribution of Elasticsearch.</p>
|
||||
<p class="euiTitle euiTitle--small">To use the full set of free features in this distribution of Kibana, please update Elasticsearch to the <a href="https://www.elastic.co/downloads/elasticsearch">default distribution</a>.</p>
|
||||
<p>Refresh to try again.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="reactLoginRoot" />
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { parse } from 'url';
|
||||
import { get } from 'lodash';
|
||||
import 'ui/autoload/styles';
|
||||
import 'plugins/security/views/login/login.less';
|
||||
import chrome from 'ui/chrome';
|
||||
import { parseNext } from 'plugins/security/lib/parse_next';
|
||||
import template from 'plugins/security/views/login/login.html';
|
||||
|
||||
const messageMap = {
|
||||
SESSION_EXPIRED: 'Your session has expired. Please log in again.'
|
||||
};
|
||||
|
||||
chrome
|
||||
.setVisible(false)
|
||||
.setRootTemplate(template)
|
||||
.setRootController('login', function ($http, $window, secureCookies, loginState) {
|
||||
const basePath = chrome.getBasePath();
|
||||
const next = parseNext($window.location.href, basePath);
|
||||
const isSecure = !!$window.location.protocol.match(/^https/);
|
||||
const self = this;
|
||||
|
||||
function setupScope() {
|
||||
self.layout = loginState.layout;
|
||||
self.allowLogin = loginState.allowLogin;
|
||||
self.loginMessage = loginState.loginMessage;
|
||||
self.infoMessage = get(messageMap, parse($window.location.href, true).query.msg);
|
||||
self.isDisabled = !isSecure && secureCookies;
|
||||
self.isLoading = false;
|
||||
self.submit = (username, password) => {
|
||||
self.isLoading = true;
|
||||
self.error = false;
|
||||
$http.post('./api/security/v1/login', { username, password }).then(
|
||||
() => $window.location.href = next,
|
||||
() => {
|
||||
setupScope();
|
||||
self.error = true;
|
||||
self.isLoading = false;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
setupScope();
|
||||
});
|
|
@ -1,125 +1,7 @@
|
|||
@import "~ui/styles/variables/colors.less";
|
||||
@import "~ui/styles/variables/bootstrap-mods.less";
|
||||
@import "~ui/styles/variables/for-theme.less";
|
||||
@import '~plugins/xpack_main/style/main.less';
|
||||
|
||||
.application {
|
||||
background: @globalColorTeal;
|
||||
.loginWelcome::before {
|
||||
content: url(../../assets/bg_top_branded.svg);
|
||||
}
|
||||
|
||||
.inner-addon {
|
||||
position: relative;
|
||||
.loginWelcome::after {
|
||||
content: url(../../assets/bg_bottom_branded.svg);
|
||||
}
|
||||
|
||||
.inner-addon .fa {
|
||||
position: absolute;
|
||||
padding: 12px;
|
||||
pointer-events: none;
|
||||
color: @gray2;
|
||||
}
|
||||
|
||||
.left-addon .fa { left: 0;}
|
||||
.right-addon .fa { right: 0;}
|
||||
|
||||
.left-addon input, .right-addon input {
|
||||
padding-left: 35px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 50%;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
background: @globalColorLightestGray;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> div {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
padding: 20px;
|
||||
background-color: @globalColorWhite;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -60px;
|
||||
margin-top: -60px;
|
||||
text-align: center;
|
||||
|
||||
.kibanaWelcomeLogo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 10px 0px 10px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 300px;
|
||||
margin-top: 20px;
|
||||
|
||||
.form-group.error-message {
|
||||
color: @globalColorRed;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-container {
|
||||
background-color: #e00;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
background-color: @brand-info;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: white;
|
||||
font-size: 1.25em;
|
||||
border: none;
|
||||
margin-bottom: 0;
|
||||
padding: 5px 15px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
line-height: 1.42857143;
|
||||
border-radius: 4px;
|
||||
vertical-align: middle;
|
||||
text-transform: uppercase;
|
||||
background-color: #006E8A;
|
||||
|
||||
&:focus:not(:disabled),
|
||||
&:hover:not(:disabled) {
|
||||
background-color: darken(#006E8A, 10%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
input.form-control {
|
||||
font-size: 1.125em;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.loginErrorEsUnavailable,
|
||||
.loginErrorXpackUnavailable {
|
||||
width: 600px;
|
||||
z-index: 10;
|
||||
}
|
62
x-pack/plugins/security/public/views/login/login.tsx
Normal file
62
x-pack/plugins/security/public/views/login/login.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { parseNext } from 'plugins/security/lib/parse_next';
|
||||
import { LoginPage } from 'plugins/security/views/login/components';
|
||||
// @ts-ignore
|
||||
import template from 'plugins/security/views/login/login.html';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import 'ui/autoload/styles';
|
||||
import chrome from 'ui/chrome';
|
||||
import { parse } from 'url';
|
||||
import { LoginState } from '../../../common/login_state';
|
||||
import './login.less';
|
||||
const messageMap = {
|
||||
SESSION_EXPIRED: 'Your session has timed out. Please log in again.',
|
||||
};
|
||||
|
||||
interface AnyObject {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
(chrome as AnyObject)
|
||||
.setVisible(false)
|
||||
.setRootTemplate(template)
|
||||
.setRootController(
|
||||
'login',
|
||||
(
|
||||
$scope: AnyObject,
|
||||
$http: AnyObject,
|
||||
$window: AnyObject,
|
||||
secureCookies: boolean,
|
||||
loginState: LoginState
|
||||
) => {
|
||||
const basePath = chrome.getBasePath();
|
||||
const next = parseNext($window.location.href, basePath);
|
||||
const isSecure = !!$window.location.protocol.match(/^https/);
|
||||
|
||||
$scope.$$postDigest(() => {
|
||||
const domNode = document.getElementById('reactLoginRoot');
|
||||
|
||||
const msgQueryParam = parse($window.location.href, true).query.msg || '';
|
||||
|
||||
render(
|
||||
<LoginPage
|
||||
http={$http}
|
||||
window={$window}
|
||||
infoMessage={get(messageMap, msgQueryParam)}
|
||||
loginState={loginState}
|
||||
isSecureConnection={isSecure}
|
||||
requiresSecureConnection={secureCookies}
|
||||
next={next}
|
||||
/>,
|
||||
domNode
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -13,7 +13,6 @@
|
|||
* security in roles.
|
||||
* @property {boolean} allowRoleFieldLevelSecurity Indicates whether we allow users to define field level security
|
||||
* in roles
|
||||
* @property {string} [loginMessage] Message to show at the login page.
|
||||
* @property {string} [linksMessage] Message to show when security links are clicked throughout the kibana app.
|
||||
*/
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
it('displays message if login fails', async () => {
|
||||
await PageObjects.security.loginPage.login('wrong-user', 'wrong-password', { expectSuccess: false });
|
||||
const errorMessage = await PageObjects.security.loginPage.getErrorMessage();
|
||||
expect(errorMessage).to.be('Oops! Error. Try again.');
|
||||
expect(errorMessage).to.be('Invalid username or password. Please try again.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
"plugins/xpack_main/*": [
|
||||
"x-pack/plugins/xpack_main/public/*"
|
||||
],
|
||||
"plugins/security/*": [
|
||||
"x-pack/plugins/security/public/*"
|
||||
],
|
||||
"plugins/spaces/*": [
|
||||
"x-pack/plugins/spaces/public/*"
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue