Add a welcome screen for new Kibana instances (#21353)

Add a welcome screen to Kibana home if this is a new Kibana instance.
New is determined by whether or not there are any index patterns
defined. Local storage is used to retain the user's decision to hide
the welcome screen.
This commit is contained in:
Chris Davies 2018-08-16 13:05:29 -04:00 committed by GitHub
parent 4ecdad2ec7
commit a73a928dea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 997 additions and 216 deletions

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1169px" height="880px" viewBox="0 0 1169 880" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 51.2 (57519) - http://www.bohemiancoding.com/sketch -->
<title>E2D4A2B4-5E17-4ADE-80E4-31B9CAF520F4</title>
<desc>Created with sketchtool.</desc>
<defs>
<radialGradient cx="0%" cy="0%" fx="0%" fy="0%" r="155.5077%" gradientTransform="translate(0.000000,0.000000),scale(0.839695,1.000000),rotate(40.020000),translate(-0.000000,-0.000000)" id="radialGradient-1">
<stop stop-color="#FFFFFF" stop-opacity="0.5" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.5" offset="100%"></stop>
</radialGradient>
<polygon id="path-2" points="0 0 1048 880 0 880"></polygon>
<linearGradient x1="98.9240878%" y1="48.9240878%" x2="7.15652071%" y2="48.9240878%" id="linearGradient-3">
<stop stop-color="#DFDDDD" stop-opacity="0.25" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0.2" offset="100%"></stop>
</linearGradient>
<linearGradient x1="0%" y1="47.4208152%" x2="100%" y2="47.4208152%" id="linearGradient-4">
<stop stop-color="#FFFFFF" stop-opacity="0.6" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.25" offset="100%"></stop>
</linearGradient>
<polygon id="path-5" points="560 364 1169 880 560 880"></polygon>
<linearGradient x1="-3.33066907e-14%" y1="3.38788096e-14%" x2="100%" y2="100%" id="linearGradient-6">
<stop stop-color="#FFFFFF" stop-opacity="0.2" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<radialGradient cx="0%" cy="0%" fx="0%" fy="0%" r="127.620365%" gradientTransform="translate(0.000000,0.000000),scale(0.932203,1.000000),rotate(42.990446),translate(-0.000000,-0.000000)" id="radialGradient-7">
<stop stop-color="#BBBBBB" stop-opacity="0.1" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.5" offset="100%"></stop>
</radialGradient>
<polygon id="path-8" points="-12 538 342 868 -12 868"></polygon>
<linearGradient x1="0%" y1="0%" x2="100%" y2="100%" id="linearGradient-9">
<stop stop-color="#FFFFFF" stop-opacity="0.4" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<radialGradient cx="0%" cy="0%" fx="0%" fy="0%" r="148.368294%" gradientTransform="translate(0.000000,0.000000),scale(1.000000,0.912371),rotate(47.623578),translate(-0.000000,-0.000000)" id="radialGradient-10">
<stop stop-color="#FFFFFF" stop-opacity="0.101024683" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.15" offset="100%"></stop>
</radialGradient>
<path d="M197,880 L374,880 C365.385564,795.927984 296.005151,723.868711 197,686 L197,880 Z" id="path-11"></path>
<radialGradient cx="0%" cy="0%" fx="0%" fy="0%" r="127.620365%" gradientTransform="translate(0.000000,0.000000),scale(1.000000,0.932203),rotate(47.009554),translate(-0.000000,-0.000000)" id="radialGradient-12">
<stop stop-color="#BBBBBB" stop-opacity="0.1" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.5" offset="100%"></stop>
</radialGradient>
<polygon id="path-13" points="165 703 330 880 165 880"></polygon>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="background/full/branded-alt" transform="translate(0.000000, -147.000000)">
<g id="bg-bottom-branded" transform="translate(0.000000, 147.000000)">
<g id="shape">
<use fill="#F5F5F5" xlink:href="#path-2"></use>
<use fill="url(#radialGradient-1)" style="mix-blend-mode: overlay;" xlink:href="#path-2"></use>
</g>
<g id="shape" opacity="0.1">
<use fill="url(#linearGradient-3)" xlink:href="#path-5"></use>
<use fill="url(#linearGradient-4)" style="mix-blend-mode: overlay;" xlink:href="#path-5"></use>
</g>
<g id="shape" opacity="0.65" transform="translate(165.000000, 703.000000) scale(-1, 1) rotate(-90.000000) translate(-165.000000, -703.000000) ">
<use fill="#DD0A73" xlink:href="#path-8"></use>
<use fill="url(#linearGradient-6)" style="mix-blend-mode: overlay;" xlink:href="#path-8"></use>
<use fill="url(#radialGradient-7)" style="mix-blend-mode: overlay;" xlink:href="#path-8"></use>
</g>
<g id="shape" opacity="0.65" transform="translate(285.500000, 783.000000) scale(-1, -1) rotate(180.000000) translate(-285.500000, -783.000000) ">
<use fill="#017F75" xlink:href="#path-11"></use>
<use fill="url(#linearGradient-9)" style="mix-blend-mode: overlay;" xlink:href="#path-11"></use>
<use fill="url(#radialGradient-10)" style="mix-blend-mode: overlay;" xlink:href="#path-11"></use>
</g>
<g id="shape" opacity="0.15">
<use fill="#353535" xlink:href="#path-13"></use>
<use fill="url(#linearGradient-6)" style="mix-blend-mode: overlay;" xlink:href="#path-13"></use>
<use fill="url(#radialGradient-12)" style="mix-blend-mode: overlay;" xlink:href="#path-13"></use>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="400px" height="373px" viewBox="0 0 400 373" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 51.2 (57519) - http://www.bohemiancoding.com/sketch -->
<title>7901612E-113D-4EE2-89F8-C5AEAD9876B6</title>
<desc>Created with sketchtool.</desc>
<defs>
<linearGradient x1="10.7931708%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
<stop stop-color="#FFFFFF" stop-opacity="0.5" offset="0%"></stop>
<stop stop-color="#E2E2E2" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<radialGradient cx="0%" cy="0%" fx="0%" fy="0%" r="146.629187%" gradientTransform="translate(0.000000,0.000000),scale(0.932500,1.000000),rotate(42.999538),translate(-0.000000,-0.000000)" id="radialGradient-2">
<stop stop-color="#FFFFFF" stop-opacity="0.5" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.2" offset="100%"></stop>
</radialGradient>
<polygon id="path-3" points="400 373 0 0 400 4.61930179e-14"></polygon>
<linearGradient x1="0%" y1="0%" x2="100%" y2="100%" id="linearGradient-4">
<stop stop-color="#FFFFFF" stop-opacity="0.6" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<radialGradient cx="0%" cy="0%" fx="0%" fy="0%" r="146.09634%" gradientTransform="translate(0.000000,0.000000),scale(0.938889,1.000000),rotate(43.194713),translate(-0.000000,-0.000000)" id="radialGradient-5">
<stop stop-color="#FFFFFF" stop-opacity="0.15" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.5" offset="100%"></stop>
</radialGradient>
<polygon id="path-6" points="400 169 220 0 400 2.09292762e-14"></polygon>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="background/full/branded-alt" transform="translate(-1040.000000, 0.000000)">
<g id="bg-top-branded" transform="translate(1040.000000, 0.000000)">
<g id="shape" opacity="0.1">
<use fill="url(#linearGradient-1)" xlink:href="#path-3"></use>
<use fill="url(#radialGradient-2)" style="mix-blend-mode: overlay;" xlink:href="#path-3"></use>
</g>
<g id="shape" opacity="0.85">
<use fill="#F5F5F5" xlink:href="#path-6"></use>
<use fill="url(#linearGradient-4)" style="mix-blend-mode: overlay;" xlink:href="#path-6"></use>
<use fill="url(#radialGradient-5)" style="mix-blend-mode: overlay;" xlink:href="#path-6"></use>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`directories should not render directory entry when showOnHomePage is false 1`] = `
exports[`home directories should not render directory entry when showOnHomePage is false 1`] = `
<EuiPage
className="home"
restrictWidth={false}
@ -118,7 +118,7 @@ exports[`directories should not render directory entry when showOnHomePage is fa
</EuiPage>
`;
exports[`directories should render ADMIN directory entry in "Manage" panel 1`] = `
exports[`home directories should render ADMIN directory entry in "Manage" panel 1`] = `
<EuiPage
className="home"
restrictWidth={false}
@ -255,7 +255,7 @@ exports[`directories should render ADMIN directory entry in "Manage" panel 1`] =
</EuiPage>
`;
exports[`directories should render DATA directory entry in "Explore Data" panel 1`] = `
exports[`home directories should render DATA directory entry in "Explore Data" panel 1`] = `
<EuiPage
className="home"
restrictWidth={false}
@ -392,7 +392,7 @@ exports[`directories should render DATA directory entry in "Explore Data" panel
</EuiPage>
`;
exports[`isNewKibanaInstance should safely handle execeptions 1`] = `
exports[`home isNewKibanaInstance should safely handle execeptions 1`] = `
<EuiPage
className="home"
restrictWidth={false}
@ -510,7 +510,7 @@ exports[`isNewKibanaInstance should safely handle execeptions 1`] = `
</EuiPage>
`;
exports[`isNewKibanaInstance should set isNewKibanaInstance to false when there are index patterns 1`] = `
exports[`home isNewKibanaInstance should set isNewKibanaInstance to false when there are index patterns 1`] = `
<EuiPage
className="home"
restrictWidth={false}
@ -628,7 +628,7 @@ exports[`isNewKibanaInstance should set isNewKibanaInstance to false when there
</EuiPage>
`;
exports[`isNewKibanaInstance should set isNewKibanaInstance to true when there are no index patterns 1`] = `
exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when there are no index patterns 1`] = `
<EuiPage
className="home"
restrictWidth={false}
@ -746,7 +746,7 @@ exports[`isNewKibanaInstance should set isNewKibanaInstance to true when there a
</EuiPage>
`;
exports[`should not contain RecentlyAccessed panel when there is no recentlyAccessed history 1`] = `
exports[`home should not contain RecentlyAccessed panel when there is no recentlyAccessed history 1`] = `
<EuiPage
className="home"
restrictWidth={false}
@ -864,7 +864,7 @@ exports[`should not contain RecentlyAccessed panel when there is no recentlyAcce
</EuiPage>
`;
exports[`should render home component 1`] = `
exports[`home should render home component 1`] = `
<EuiPage
className="home"
restrictWidth={false}
@ -997,3 +997,364 @@ exports[`should render home component 1`] = `
</EuiPageBody>
</EuiPage>
`;
exports[`home welcome should show the normal home page if loading fails 1`] = `
<EuiPage
className="home"
restrictWidth={false}
>
<EuiPageBody
restrictWidth={false}
>
<AddData
apmUiEnabled={true}
isNewKibanaInstance={false}
/>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<EuiTitle
size="m"
>
<h3>
Visualize and Explore Data
</h3>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGrid
columns={2}
gutterSize="l"
responsive={true}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
>
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<EuiTitle
size="m"
>
<h3>
Manage and Administer the Elastic Stack
</h3>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGrid
columns={2}
gutterSize="l"
responsive={true}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="center"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiText
grow={true}
>
<p>
Didnt find what you were looking for?
</p>
</EuiText>
<EuiSpacer
size="s"
/>
<EuiButton
color="primary"
fill={false}
href="#/home/feature_directory"
iconSide="left"
type="button"
>
View full directory of Kibana plugins
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</EuiPage>
`;
exports[`home welcome should show the normal home page if welcome screen is disabled locally 1`] = `
<EuiPage
className="home"
restrictWidth={false}
>
<EuiPageBody
restrictWidth={false}
>
<AddData
apmUiEnabled={true}
isNewKibanaInstance={false}
/>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<EuiTitle
size="m"
>
<h3>
Visualize and Explore Data
</h3>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGrid
columns={2}
gutterSize="l"
responsive={true}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
>
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<EuiTitle
size="m"
>
<h3>
Manage and Administer the Elastic Stack
</h3>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGrid
columns={2}
gutterSize="l"
responsive={true}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="center"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiText
grow={true}
>
<p>
Didnt find what you were looking for?
</p>
</EuiText>
<EuiSpacer
size="s"
/>
<EuiButton
color="primary"
fill={false}
href="#/home/feature_directory"
iconSide="left"
type="button"
>
View full directory of Kibana plugins
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</EuiPage>
`;
exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = `
<Welcome
onSkip={[Function]}
urlBasePath="goober"
/>
`;
exports[`home welcome stores skip welcome setting if skipped 1`] = `
<EuiPage
className="home"
restrictWidth={false}
>
<EuiPageBody
restrictWidth={false}
>
<AddData
apmUiEnabled={true}
isNewKibanaInstance={true}
/>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<EuiTitle
size="m"
>
<h3>
Visualize and Explore Data
</h3>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGrid
columns={2}
gutterSize="l"
responsive={true}
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
>
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<EuiTitle
size="m"
>
<h3>
Manage and Administer the Elastic Stack
</h3>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGrid
columns={2}
gutterSize="l"
responsive={true}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="center"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiText
grow={true}
>
<p>
Didnt find what you were looking for?
</p>
</EuiText>
<EuiSpacer
size="s"
/>
<EuiButton
color="primary"
fill={false}
href="#/home/feature_directory"
iconSide="left"
type="button"
>
View full directory of Kibana plugins
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</EuiPage>
`;

View file

@ -36,12 +36,26 @@ import {
EuiPageBody,
} from '@elastic/eui';
import { Welcome } from './welcome';
import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
export class Home extends Component {
const KEY_ENABLE_WELCOME = 'home:welcome:show';
state = {
isNewKibanaInstance: false,
export class Home extends Component {
constructor(props) {
super(props);
const isWelcomeEnabled = props.localStorage.getItem(KEY_ENABLE_WELCOME) !== 'false';
this.state = {
// If welcome is enabled, we wait for loading to complete
// before rendering. This prevents an annoying flickering
// effect where home renders, and then a few ms after, the
// welcome screen fades in.
isLoading: isWelcomeEnabled,
isNewKibanaInstance: false,
isWelcomeEnabled,
};
}
componentWillUnmount() {
@ -54,37 +68,52 @@ export class Home extends Component {
}
fetchIsNewKibanaInstance = async () => {
let resp;
try {
resp = await this.props.find({
// Set a max-time on this query so we don't hang the page too long...
// Worst case, we don't show the welcome screen when we should.
setTimeout(() => {
if (this.state.isLoading) {
this.setState({ isWelcomeEnabled: false });
}
}, 500);
const resp = await this.props.find({
type: 'index-pattern',
fields: ['title'],
search: `*`,
search_fields: ['title'],
perPage: 1
perPage: 1,
});
} catch (error) {
// ignore error - find is not critical for page functioning,
// just used to add some extra styling when there are no index-patterns
return;
this.endLoading({ isNewKibanaInstance: resp.total === 0 });
} catch (err) {
// An error here is relatively unimportant, as it only means we don't provide
// some UI niceties.
this.endLoading();
}
};
if (!this._isMounted) {
return;
endLoading = (state = {}) => {
if (this._isMounted) {
this.setState({
...state,
isLoading: false,
});
}
};
this.setState({
isNewKibanaInstance: resp.total === 0
});
}
skipWelcome = () => {
this.props.localStorage.setItem(KEY_ENABLE_WELCOME, 'false');
this._isMounted && this.setState({ isWelcomeEnabled: false });
};
renderDirectories = (category) => {
renderDirectories = category => {
const { addBasePath, directories } = this.props;
return directories
.filter((directory) => {
.filter(directory => {
return directory.showOnHomePage && directory.category === category;
})
.map((directory) => {
.map(directory => {
return (
<EuiFlexItem style={{ minHeight: 64 }} key={directory.id}>
<Synopsis
@ -98,17 +127,14 @@ export class Home extends Component {
});
};
render() {
renderNormal() {
const { apmUiEnabled, recentlyAccessed } = this.props;
let recentlyAccessedPanel;
if (recentlyAccessed.length > 0) {
recentlyAccessedPanel = (
<Fragment>
<RecentlyAccessed
recentlyAccessed={recentlyAccessed}
/>
<RecentlyAccessed recentlyAccessed={recentlyAccessed} />
<EuiSpacer size="l" />
</Fragment>
);
@ -117,7 +143,6 @@ export class Home extends Component {
return (
<EuiPage className="home">
<EuiPageBody>
{recentlyAccessedPanel}
<AddData
@ -131,26 +156,22 @@ export class Home extends Component {
<EuiFlexItem>
<EuiPanel paddingSize="l">
<EuiTitle>
<h3>
Visualize and Explore Data
</h3>
<h3>Visualize and Explore Data</h3>
</EuiTitle>
<EuiSpacer size="m"/>
<EuiSpacer size="m" />
<EuiFlexGrid columns={2}>
{ this.renderDirectories(FeatureCatalogueCategory.DATA) }
{this.renderDirectories(FeatureCatalogueCategory.DATA)}
</EuiFlexGrid>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel paddingSize="l">
<EuiTitle>
<h3>
Manage and Administer the Elastic Stack
</h3>
<h3>Manage and Administer the Elastic Stack</h3>
</EuiTitle>
<EuiSpacer size="m"/>
<EuiSpacer size="m" />
<EuiFlexGrid columns={2}>
{ this.renderDirectories(FeatureCatalogueCategory.ADMIN) }
{this.renderDirectories(FeatureCatalogueCategory.ADMIN)}
</EuiFlexGrid>
</EuiPanel>
</EuiFlexItem>
@ -161,14 +182,10 @@ export class Home extends Component {
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiText>
<p>
Didnt find what you were looking for?
</p>
<p>Didnt find what you were looking for?</p>
</EuiText>
<EuiSpacer size="s" />
<EuiButton
href="#/home/feature_directory"
>
<EuiButton href="#/home/feature_directory">
View full directory of Kibana plugins
</EuiButton>
</EuiFlexItem>
@ -177,20 +194,54 @@ export class Home extends Component {
</EuiPage>
);
}
// For now, loading is just an empty page, as we'll show something
// in 250ms, no matter what, and a blank page prevents an odd flicker effect.
renderLoading() {
return '';
}
renderWelcome() {
return (
<Welcome
onSkip={this.skipWelcome}
urlBasePath={this.props.urlBasePath}
/>
);
}
render() {
const { isLoading, isWelcomeEnabled, isNewKibanaInstance } = this.state;
if (isWelcomeEnabled) {
if (isLoading) {
return this.renderLoading();
}
if (isNewKibanaInstance) {
return this.renderWelcome();
}
}
return this.renderNormal();
}
}
Home.propTypes = {
addBasePath: PropTypes.func.isRequired,
directories: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
showOnHomePage: PropTypes.bool.isRequired,
category: PropTypes.string.isRequired
})),
directories: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
showOnHomePage: PropTypes.bool.isRequired,
category: PropTypes.string.isRequired,
})
),
apmUiEnabled: PropTypes.bool.isRequired,
recentlyAccessed: PropTypes.arrayOf(recentlyAccessedShape).isRequired,
find: PropTypes.func.isRequired,
localStorage: PropTypes.object.isRequired,
urlBasePath: PropTypes.string.isRequired,
};

View file

@ -18,175 +18,208 @@
*/
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import { Home } from './home';
import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
const addBasePath = (url) => { return `base_path/${url}`; };
const findMock = () => {
return Promise.resolve({ total: 1 });
};
describe('home', () => {
let defaultProps;
test('should render home component', () => {
const recentlyAccessed = [
{
label: 'my vis',
link: 'link_to_my_vis',
id: '1'
}
];
const component = shallow(<Home
addBasePath={addBasePath}
directories={[]}
apmUiEnabled={true}
recentlyAccessed={recentlyAccessed}
find={findMock}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should not contain RecentlyAccessed panel when there is no recentlyAccessed history', () => {
const component = shallow(<Home
addBasePath={addBasePath}
directories={[]}
apmUiEnabled={true}
recentlyAccessed={[]}
find={findMock}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
describe('directories', () => {
test('should render DATA directory entry in "Explore Data" panel', () => {
const directoryEntry = {
id: 'dashboard',
title: 'Dashboard',
description: 'Display and share a collection of visualizations and saved searches.',
icon: 'dashboardApp',
path: 'dashboard_landing_page',
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA
beforeEach(() => {
defaultProps = {
recentlyAccessed: [],
directories: [],
apmUiEnabled: true,
kibanaVersion: '99.2.1',
addBasePath(url) {
return `base_path/${url}`;
},
find() {
return Promise.resolve({ total: 1 });
},
loadingCount: {
increment: sinon.mock(),
decrement: sinon.mock(),
},
localStorage: {
getItem: sinon.spy((path) => {
expect(path).toEqual('home:welcome:show');
return 'false';
}),
setItem: sinon.mock(),
},
urlBasePath: 'goober',
};
const component = shallow(<Home
addBasePath={addBasePath}
directories={[directoryEntry]}
apmUiEnabled={true}
recentlyAccessed={[]}
find={findMock}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should render ADMIN directory entry in "Manage" panel', () => {
const directoryEntry = {
id: 'index_patterns',
title: 'Index Patterns',
description: 'Manage the index patterns that help retrieve your data from Elasticsearch.',
icon: 'indexPatternApp',
path: 'index_management_landing_page',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN
};
async function renderHome(props = {}) {
const component = shallow(<Home
addBasePath={addBasePath}
directories={[directoryEntry]}
apmUiEnabled={true}
recentlyAccessed={[]}
find={findMock}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should not render directory entry when showOnHomePage is false', () => {
const directoryEntry = {
id: 'management',
title: 'Management',
description: 'Your center console for managing the Elastic Stack.',
icon: 'managementApp',
path: 'management_landing_page',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN
};
const component = shallow(<Home
addBasePath={addBasePath}
directories={[directoryEntry]}
apmUiEnabled={true}
recentlyAccessed={[]}
find={findMock}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
});
describe('isNewKibanaInstance', () => {
test('should set isNewKibanaInstance to true when there are no index patterns', async () => {
const component = shallow(<Home
addBasePath={addBasePath}
directories={[]}
apmUiEnabled={true}
recentlyAccessed={[]}
find={
() => {
return Promise.resolve({ total: 0 });
}
}
{...defaultProps}
{...props}
/>);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should set isNewKibanaInstance to false when there are index patterns', async () => {
const component = shallow(<Home
addBasePath={addBasePath}
directories={[]}
apmUiEnabled={true}
recentlyAccessed={[]}
find={
() => {
return Promise.resolve({ total: 1 });
}
}
/>);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot(); // eslint-disable-line
return component;
}
test('should render home component', async () => {
const component = await renderHome({
recentlyAccessed: [
{
label: 'my vis',
link: 'link_to_my_vis',
id: '1'
}
],
});
expect(component).toMatchSnapshot();
});
test('should safely handle execeptions', async () => {
const component = shallow(<Home
addBasePath={addBasePath}
directories={[]}
apmUiEnabled={true}
recentlyAccessed={[]}
find={
() => {
test('should not contain RecentlyAccessed panel when there is no recentlyAccessed history', async () => {
const component = await renderHome({
recentlyAccessed: [],
});
expect(component).toMatchSnapshot();
});
describe('directories', () => {
test('should render DATA directory entry in "Explore Data" panel', async () => {
const directoryEntry = {
id: 'dashboard',
title: 'Dashboard',
description: 'Display and share a collection of visualizations and saved searches.',
icon: 'dashboardApp',
path: 'dashboard_landing_page',
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA
};
const component = await renderHome({
directories: [directoryEntry],
});
expect(component).toMatchSnapshot();
});
test('should render ADMIN directory entry in "Manage" panel', async () => {
const directoryEntry = {
id: 'index_patterns',
title: 'Index Patterns',
description: 'Manage the index patterns that help retrieve your data from Elasticsearch.',
icon: 'indexPatternApp',
path: 'index_management_landing_page',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN
};
const component = await renderHome({
directories: [directoryEntry],
});
expect(component).toMatchSnapshot();
});
test('should not render directory entry when showOnHomePage is false', async () => {
const directoryEntry = {
id: 'management',
title: 'Management',
description: 'Your center console for managing the Elastic Stack.',
icon: 'managementApp',
path: 'management_landing_page',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN
};
const component = await renderHome({
directories: [directoryEntry],
});
expect(component).toMatchSnapshot();
});
});
describe('welcome', () => {
test('should show the welcome screen if enabled, and there are no index patterns defined', async () => {
defaultProps.localStorage.getItem = sinon.spy(() => 'true');
const component = await renderHome({
find: () => Promise.resolve({ total: 0 }),
});
sinon.assert.calledOnce(defaultProps.localStorage.getItem);
expect(component).toMatchSnapshot();
});
test('stores skip welcome setting if skipped', async () => {
defaultProps.localStorage.getItem = sinon.spy(() => 'true');
const component = await renderHome({
find: () => Promise.resolve({ total: 0 }),
});
component.instance().skipWelcome();
component.update();
sinon.assert.calledWith(defaultProps.localStorage.setItem, 'home:welcome:show', 'false');
expect(component).toMatchSnapshot();
});
test('should show the normal home page if loading fails', async () => {
defaultProps.localStorage.getItem = sinon.spy(() => 'true');
const component = await renderHome({
find: () => Promise.reject('Doh!'),
});
expect(component).toMatchSnapshot();
});
test('should show the normal home page if welcome screen is disabled locally', async () => {
defaultProps.localStorage.getItem = sinon.spy(() => 'false');
const component = await renderHome();
expect(component).toMatchSnapshot();
});
});
describe('isNewKibanaInstance', () => {
test('should set isNewKibanaInstance to true when there are no index patterns', async () => {
const component = await renderHome({
find: () => Promise.resolve({ total: 0 }),
});
expect(component).toMatchSnapshot();
});
test('should set isNewKibanaInstance to false when there are index patterns', async () => {
const component = await renderHome({
find: () => Promise.resolve({ total: 1 }),
});
expect(component).toMatchSnapshot();
});
test('should safely handle execeptions', async () => {
const component = await renderHome({
find: () => {
throw new Error('simulated find error');
}
}
/>);
},
});
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot(); // eslint-disable-line
expect(component).toMatchSnapshot();
});
});
});

View file

@ -100,6 +100,8 @@ export function HomeApp({
apmUiEnabled={apmUiEnabled}
recentlyAccessed={recentlyAccessed}
find={savedObjectsClient.find}
localStorage={localStorage}
urlBasePath={chrome.getBasePath()}
/>
</Route>
</Switch>

View file

@ -0,0 +1,115 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* The UI and related logic for the welcome screen that *should* show only
* when it is enabled (the default) and there is no Kibana-consumed data
* in Elasticsearch.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiCard,
EuiTitle,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiIcon,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
/**
* Shows a full-screen welcome page that gives helpful quick links to beginners.
*/
export class Welcome extends React.Component {
hideOnEsc = (e) => {
if (e.key === 'Escape') {
this.props.onSkip();
}
};
componentDidMount() {
document.addEventListener('keydown', this.hideOnEsc);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.hideOnEsc);
}
render() {
const { urlBasePath, onSkip } = this.props;
return (
<div className="home-welcome">
<header className="home-welcome-header">
<div className="home-welcome-content eui-textCenter">
<EuiSpacer size="xxl" />
<span className="home-welcome-logo">
<EuiIcon type="logoKibana" size="xxl" />
</span>
<EuiTitle size="l" className="home-welcome-title">
<h1>Welcome to Kibana</h1>
</EuiTitle>
<EuiText size="s" className="welcome-subtitle">Your window into the Elastic Stack</EuiText>
<EuiSpacer size="xl" />
</div>
</header>
<div className="home-welcome-content home-welcome-body">
<EuiFlexGroup gutterSize="l">
<EuiFlexItem>
<EuiCard
image={`${urlBasePath}/plugins/kibana/assets/illo_dashboard.png`}
textAlign="left"
title="Let's get started"
description="We noticed that you don't have any data in your cluster.
You can try our sample data and dashboards or jump in with your own data."
footer={(
<footer>
<EuiButton
fill
className="home-welcome-footer-action"
href="#/home/tutorial_directory/sampleData"
>
Try our sample data
</EuiButton>
<EuiButtonEmpty
className="home-welcome-footer-action"
onClick={onSkip}
data-test-subj="skipWelcomeScreen"
>
Explore on my own
</EuiButtonEmpty>
</footer>
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</div>
);
}
}
Welcome.propTypes = {
urlBasePath: PropTypes.string.isRequired,
onSkip: PropTypes.func.isRequired,
};

View file

@ -26,3 +26,83 @@ home-app {
.sampleDataSetCard {
flex-grow: 0;
}
.home-welcome {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100000;
background: inherit;
// When sassified, should pull in EUI colors: $euiColorLightestShade, $euiColorEmptyShade
background-image: linear-gradient(0deg, @globalColorLightestGray 0%, white 100%);
color: inherit;
opacity: 0;
overflow: auto;
animation: homeFadeIn 0.5s ease-in 0s forwards;
}
.home-welcome::before {
content: url(../assets/bg_top_branded.svg);
position: absolute;
top: 0;
right: 0;
z-index: 1;
}
.home-welcome::after {
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;
}
.home-welcome-header {
position: relative;
padding: 32px;
z-index: 10;
}
.home-welcome-logo {
display: inline-block;
margin-bottom: 24px;
background-color: white;
border-radius: 100%;
padding: 16px;
box-shadow: 0 4px 16px -6px rgba(0, 0, 0, 0.75);
}
.home-welcome-title {
color: inherit;
font-weight: 400;
}
.home-welcome-footer-action {
margin-right: 8px;
}
.welcome-subtitle {
opacity: 0.75;
}
.home-welcome-content {
position: relative;
margin: auto;
max-width: 512px;
padding-left: 32px;
padding-right: 32px;
z-index: 10;
}
@keyframes homeFadeIn {
from {
opacity: 0;
transform: translateY(200px), scale(0.75);
}
to {
opacity: 1;
transform: translateY(0), scale(1);
}
}

View file

@ -20,14 +20,14 @@
import { StringUtils } from 'ui/utils/string_utils';
const names = {
'general': 'General',
'timelion': 'Timelion',
'notifications': 'Notifications',
'visualizations': 'Visualizations',
'discover': 'Discover',
'dashboard': 'Dashboard',
'reporting': 'Reporting',
'search': 'Search',
general: 'General',
timelion: 'Timelion',
notifications: 'Notifications',
visualizations: 'Visualizations',
discover: 'Discover',
dashboard: 'Dashboard',
reporting: 'Reporting',
search: 'Search',
};
export function getCategoryName(category) {

View file

@ -63,6 +63,18 @@ export function HomePageProvider({ getService }) {
await testSubjects.click(`launchSampleDataSet${id}`);
}
// When logging into a brand new Kibana instance, the welcome screen
// may pop up. It may not, depending on the speed of the test, so it
// pays to check for the welcome screen and hide it in any test that
// hits the Kibana home page.
isWelcomeShowing() {
return testSubjects.exists('skipWelcomeScreen');
}
async hideWelcomeScreen() {
await testSubjects.click('skipWelcomeScreen');
}
async loadSavedObjects() {
await retry.try(async () => {
await testSubjects.click('loadSavedObjects');

View file

@ -16,7 +16,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
const testSubjects = getService('testSubjects');
const esArchiver = getService('esArchiver');
const defaultFindTimeout = config.get('timeouts.find');
const PageObjects = getPageObjects(['common', 'header', 'settings']);
const PageObjects = getPageObjects(['common', 'header', 'settings', 'home']);
class LoginPage {
async login(username, password) {
@ -72,12 +72,24 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
async logout() {
log.debug('SecurityPage.logout');
const logoutLinkExists = await find.existsByLinkText('Logout');
const [isWelcomeShowing, logoutLinkExists] = await Promise.all([
PageObjects.home.isWelcomeShowing(),
find.existsByLinkText('Logout'),
]);
if (!logoutLinkExists) {
log.debug('Logout not found');
return;
}
// This sometimes happens when hitting the home screen on a brand new / empty
// Kibana instance. It may not *always* happen, depending on how
// long it takes the home screen to query Elastic to see if it's a
// new Kibana instance.
if (isWelcomeShowing) {
await PageObjects.home.hideWelcomeScreen();
}
await find.clickByLinkText('Logout');
await retry.try(async () => {