mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
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  ## Session expired  ## No connection to Elasticsearch  ## Insecure connection  Closes #20015
This commit is contained in:
parent
6521387e27
commit
c4b0de7bff
30 changed files with 1481 additions and 250 deletions
|
@ -18,24 +18,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function ShieldPageProvider({ getService }) {
|
export function ShieldPageProvider({ getService }) {
|
||||||
const remote = getService('remote');
|
const testSubjects = getService('testSubjects');
|
||||||
const config = getService('config');
|
|
||||||
|
|
||||||
const defaultFindTimeout = config.get('timeouts.find');
|
|
||||||
|
|
||||||
class ShieldPage {
|
class ShieldPage {
|
||||||
login(user, pwd) {
|
async login(user, pwd) {
|
||||||
return remote.setFindTimeout(defaultFindTimeout)
|
await testSubjects.setValue('loginUsername', user);
|
||||||
.findById('username')
|
await testSubjects.setValue('loginPassword', pwd);
|
||||||
.type(user)
|
await testSubjects.click('loginSubmit');
|
||||||
.then(function () {
|
|
||||||
return remote.findById('password')
|
|
||||||
.type(pwd);
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
return remote.findByCssSelector('button')
|
|
||||||
.click();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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: {
|
uiExports: {
|
||||||
chromeNavControls: ['plugins/security/views/nav_control'],
|
chromeNavControls: ['plugins/security/views/nav_control'],
|
||||||
managementSections: ['plugins/security/views/management'],
|
managementSections: ['plugins/security/views/management'],
|
||||||
|
styleSheetPaths: `${__dirname}/public/index.scss`,
|
||||||
apps: [{
|
apps: [{
|
||||||
id: 'login',
|
id: 'login',
|
||||||
title: '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';
|
import { parse } from 'url';
|
||||||
|
|
||||||
export function parseNext(href, basePath = '') {
|
export function parseNext(href: string, basePath = '') {
|
||||||
const { query, hash } = parse(href, true);
|
const { query, hash } = parse(href, true);
|
||||||
if (!query.next) {
|
if (!query.next) {
|
||||||
return `${basePath}/`;
|
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
|
// validate that `next` is not attempting a redirect to somewhere
|
||||||
// outside of this Kibana install
|
// outside of this Kibana install
|
||||||
const { protocol, hostname, port, pathname } = parse(
|
const { protocol, hostname, port, pathname } = parse(
|
||||||
query.next,
|
next,
|
||||||
false /* parseQueryString */,
|
false /* parseQueryString */,
|
||||||
true /* slashesDenoteHost */
|
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 id="reactLoginRoot" />
|
||||||
<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>
|
|
|
@ -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";
|
.loginWelcome::before {
|
||||||
@import "~ui/styles/variables/bootstrap-mods.less";
|
content: url(../../assets/bg_top_branded.svg);
|
||||||
@import "~ui/styles/variables/for-theme.less";
|
|
||||||
@import '~plugins/xpack_main/style/main.less';
|
|
||||||
|
|
||||||
.application {
|
|
||||||
background: @globalColorTeal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner-addon {
|
.loginWelcome::after {
|
||||||
position: relative;
|
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.
|
* security in roles.
|
||||||
* @property {boolean} allowRoleFieldLevelSecurity Indicates whether we allow users to define field level security
|
* @property {boolean} allowRoleFieldLevelSecurity Indicates whether we allow users to define field level security
|
||||||
* in roles
|
* 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.
|
* @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 () => {
|
it('displays message if login fails', async () => {
|
||||||
await PageObjects.security.loginPage.login('wrong-user', 'wrong-password', { expectSuccess: false });
|
await PageObjects.security.loginPage.login('wrong-user', 'wrong-password', { expectSuccess: false });
|
||||||
const errorMessage = await PageObjects.security.loginPage.getErrorMessage();
|
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/*": [
|
"plugins/xpack_main/*": [
|
||||||
"x-pack/plugins/xpack_main/public/*"
|
"x-pack/plugins/xpack_main/public/*"
|
||||||
],
|
],
|
||||||
|
"plugins/security/*": [
|
||||||
|
"x-pack/plugins/security/public/*"
|
||||||
|
],
|
||||||
"plugins/spaces/*": [
|
"plugins/spaces/*": [
|
||||||
"x-pack/plugins/spaces/public/*"
|
"x-pack/plugins/spaces/public/*"
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue