From a73a928dea17245831c889ed1ea68611c16efd57 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Thu, 16 Aug 2018 13:05:29 -0400 Subject: [PATCH] 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. --- .../public/assets/bg_bottom_branded.svg | 74 ++++ .../kibana/public/assets/bg_top_branded.svg | 41 ++ .../kibana/public/assets/illo_dashboard.png | Bin 0 -> 11739 bytes .../__snapshots__/home.test.js.snap | 377 +++++++++++++++++- .../kibana/public/home/components/home.js | 151 ++++--- .../public/home/components/home.test.js | 319 ++++++++------- .../kibana/public/home/components/home_app.js | 2 + .../kibana/public/home/components/welcome.js | 115 ++++++ src/core_plugins/kibana/public/home/home.less | 80 ++++ .../settings/lib/get_category_name.js | 16 +- test/functional/page_objects/home_page.js | 12 + .../functional/page_objects/security_page.js | 16 +- 12 files changed, 992 insertions(+), 211 deletions(-) create mode 100644 src/core_plugins/kibana/public/assets/bg_bottom_branded.svg create mode 100644 src/core_plugins/kibana/public/assets/bg_top_branded.svg create mode 100644 src/core_plugins/kibana/public/assets/illo_dashboard.png create mode 100644 src/core_plugins/kibana/public/home/components/welcome.js diff --git a/src/core_plugins/kibana/public/assets/bg_bottom_branded.svg b/src/core_plugins/kibana/public/assets/bg_bottom_branded.svg new file mode 100644 index 0000000000000..9f6114fd5c502 --- /dev/null +++ b/src/core_plugins/kibana/public/assets/bg_bottom_branded.svg @@ -0,0 +1,74 @@ + + + + E2D4A2B4-5E17-4ADE-80E4-31B9CAF520F4 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/core_plugins/kibana/public/assets/bg_top_branded.svg b/src/core_plugins/kibana/public/assets/bg_top_branded.svg new file mode 100644 index 0000000000000..f6e1965078462 --- /dev/null +++ b/src/core_plugins/kibana/public/assets/bg_top_branded.svg @@ -0,0 +1,41 @@ + + + + 7901612E-113D-4EE2-89F8-C5AEAD9876B6 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/core_plugins/kibana/public/assets/illo_dashboard.png b/src/core_plugins/kibana/public/assets/illo_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..ef2dac5ca4144f1d5b923d62617a1328b212d852 GIT binary patch literal 11739 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 PUqcjRRHZAPnZNo!ApgyC literal 0 HcmV?d00001 diff --git a/src/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap b/src/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap index c400509856a1d..fca2261462277 100644 --- a/src/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap +++ b/src/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap @@ -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`] = ` `; -exports[`directories should render ADMIN directory entry in "Manage" panel 1`] = ` +exports[`home directories should render ADMIN directory entry in "Manage" panel 1`] = ` `; -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`] = ` `; -exports[`isNewKibanaInstance should safely handle execeptions 1`] = ` +exports[`home isNewKibanaInstance should safely handle execeptions 1`] = ` `; -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`] = ` `; -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`] = ` `; -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`] = ` `; -exports[`should render home component 1`] = ` +exports[`home should render home component 1`] = ` `; + +exports[`home welcome should show the normal home page if loading fails 1`] = ` + + + + + + + + +

+ Visualize and Explore Data +

+
+ + +
+
+ + + +

+ Manage and Administer the Elastic Stack +

+
+ + +
+
+
+ + + + +

+ Didn’t find what you were looking for? +

+
+ + + View full directory of Kibana plugins + +
+
+
+
+`; + +exports[`home welcome should show the normal home page if welcome screen is disabled locally 1`] = ` + + + + + + + + +

+ Visualize and Explore Data +

+
+ + +
+
+ + + +

+ Manage and Administer the Elastic Stack +

+
+ + +
+
+
+ + + + +

+ Didn’t find what you were looking for? +

+
+ + + View full directory of Kibana plugins + +
+
+
+
+`; + +exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = ` + +`; + +exports[`home welcome stores skip welcome setting if skipped 1`] = ` + + + + + + + + +

+ Visualize and Explore Data +

+
+ + +
+
+ + + +

+ Manage and Administer the Elastic Stack +

+
+ + +
+
+
+ + + + +

+ Didn’t find what you were looking for? +

+
+ + + View full directory of Kibana plugins + +
+
+
+
+`; diff --git a/src/core_plugins/kibana/public/home/components/home.js b/src/core_plugins/kibana/public/home/components/home.js index a138983226a9f..1d3c195bdaf1d 100644 --- a/src/core_plugins/kibana/public/home/components/home.js +++ b/src/core_plugins/kibana/public/home/components/home.js @@ -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 ( 0) { recentlyAccessedPanel = ( - + ); @@ -117,7 +143,6 @@ export class Home extends Component { return ( - {recentlyAccessedPanel} -

- Visualize and Explore Data -

+

Visualize and Explore Data

- + - { this.renderDirectories(FeatureCatalogueCategory.DATA) } + {this.renderDirectories(FeatureCatalogueCategory.DATA)}
-

- Manage and Administer the Elastic Stack -

+

Manage and Administer the Elastic Stack

- + - { this.renderDirectories(FeatureCatalogueCategory.ADMIN) } + {this.renderDirectories(FeatureCatalogueCategory.ADMIN)}
@@ -161,14 +182,10 @@ export class Home extends Component { -

- Didn’t find what you were looking for? -

+

Didn’t find what you were looking for?

- + View full directory of Kibana plugins
@@ -177,20 +194,54 @@ export class Home extends Component {
); } + + // 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 ( + + ); + } + + 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, }; diff --git a/src/core_plugins/kibana/public/home/components/home.test.js b/src/core_plugins/kibana/public/home/components/home.test.js index 590a8727d3f25..8c901f34be1ac 100644 --- a/src/core_plugins/kibana/public/home/components/home.test.js +++ b/src/core_plugins/kibana/public/home/components/home.test.js @@ -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 }); -}; - -test('should render home component', () => { - const recentlyAccessed = [ - { - label: 'my vis', - link: 'link_to_my_vis', - id: '1' - } - ]; - const component = shallow(); - - expect(component).toMatchSnapshot(); // eslint-disable-line -}); - -test('should not contain RecentlyAccessed panel when there is no recentlyAccessed history', () => { - const component = shallow(); - - 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 +describe('home', () => { + let defaultProps; + + 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', }; + }); + async function renderHome(props = {}) { const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line - }); + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); - 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 - }; + return component; + } - const component = shallow(); + test('should render home component', async () => { + const component = await renderHome({ + recentlyAccessed: [ + { + label: 'my vis', + link: 'link_to_my_vis', + id: '1' + } + ], + }); - expect(component).toMatchSnapshot(); // eslint-disable-line + expect(component).toMatchSnapshot(); }); - 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 - }; + test('should not contain RecentlyAccessed panel when there is no recentlyAccessed history', async () => { + const component = await renderHome({ + recentlyAccessed: [], + }); - const component = shallow(); + expect(component).toMatchSnapshot(); + }); - expect(component).toMatchSnapshot(); // eslint-disable-line + 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('isNewKibanaInstance', () => { - test('should set isNewKibanaInstance to true when there are no index patterns', async () => { - const component = shallow( { - return Promise.resolve({ total: 0 }); - } - } - />); + describe('welcome', () => { + test('should show the welcome screen if enabled, and there are no index patterns defined', async () => { + defaultProps.localStorage.getItem = sinon.spy(() => 'true'); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + const component = await renderHome({ + find: () => Promise.resolve({ total: 0 }), + }); - expect(component).toMatchSnapshot(); // eslint-disable-line - }); + sinon.assert.calledOnce(defaultProps.localStorage.getItem); - test('should set isNewKibanaInstance to false when there are index patterns', async () => { - const component = shallow( { - return Promise.resolve({ total: 1 }); - } - } - />); + expect(component).toMatchSnapshot(); + }); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + 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'); - expect(component).toMatchSnapshot(); // eslint-disable-line + const component = await renderHome(); + + expect(component).toMatchSnapshot(); + }); }); - test('should safely handle execeptions', async () => { - const component = shallow( { - throw new Error('simulated find error'); - } - } - />); + describe('isNewKibanaInstance', () => { + test('should set isNewKibanaInstance to true when there are no index patterns', async () => { + const component = await renderHome({ + find: () => Promise.resolve({ total: 0 }), + }); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + 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(); + }); - expect(component).toMatchSnapshot(); // eslint-disable-line + test('should safely handle execeptions', async () => { + const component = await renderHome({ + find: () => { + throw new Error('simulated find error'); + }, + }); + + expect(component).toMatchSnapshot(); + }); }); }); + diff --git a/src/core_plugins/kibana/public/home/components/home_app.js b/src/core_plugins/kibana/public/home/components/home_app.js index e3b48c0e680ad..d277fd542f70d 100644 --- a/src/core_plugins/kibana/public/home/components/home_app.js +++ b/src/core_plugins/kibana/public/home/components/home_app.js @@ -100,6 +100,8 @@ export function HomeApp({ apmUiEnabled={apmUiEnabled} recentlyAccessed={recentlyAccessed} find={savedObjectsClient.find} + localStorage={localStorage} + urlBasePath={chrome.getBasePath()} /> diff --git a/src/core_plugins/kibana/public/home/components/welcome.js b/src/core_plugins/kibana/public/home/components/welcome.js new file mode 100644 index 0000000000000..aa10b551f4ca3 --- /dev/null +++ b/src/core_plugins/kibana/public/home/components/welcome.js @@ -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 ( +
+
+
+ + + + + +

Welcome to Kibana

+
+ Your window into the Elastic Stack + +
+
+
+ + + + + Try our sample data + + + Explore on my own + + + )} + /> + + +
+
+ ); + } +} + +Welcome.propTypes = { + urlBasePath: PropTypes.string.isRequired, + onSkip: PropTypes.func.isRequired, +}; diff --git a/src/core_plugins/kibana/public/home/home.less b/src/core_plugins/kibana/public/home/home.less index b8ce68b98a699..f2c4764a2b58b 100644 --- a/src/core_plugins/kibana/public/home/home.less +++ b/src/core_plugins/kibana/public/home/home.less @@ -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); + } +} diff --git a/src/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js b/src/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js index cad3f28a5ad16..d41c4bce089ca 100644 --- a/src/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js +++ b/src/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js @@ -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) { diff --git a/test/functional/page_objects/home_page.js b/test/functional/page_objects/home_page.js index 9b1c3d6a5c741..77205e7bc3323 100644 --- a/test/functional/page_objects/home_page.js +++ b/test/functional/page_objects/home_page.js @@ -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'); diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index c41323f3962ec..96e30e152689f 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -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 () => {