[Home] Elastic home page redesign (#70571)

Co-authored-by: Catherine Liu <catherine.liu@elastic.co>
Co-authored-by: Ryan Keairns <contactryank@gmail.com>
Co-authored-by: Catherine Liu <catherineqliu@outlook.com>
Co-authored-by: Michael Marcialis <michael.marcialis@elastic.co>
This commit is contained in:
Catherine Liu 2020-08-26 13:00:00 -07:00 committed by GitHub
parent 638df5820c
commit 532f2d70e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 4124 additions and 2954 deletions

View file

@ -147,7 +147,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
"baseUrl": "/",
"category": Object {
"euiIconType": "logoSecurity",
"id": "security",
"id": "securitySolution",
"label": "Security",
"order": 4000,
},
@ -1393,11 +1393,11 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
</EuiAccordion>
</EuiCollapsibleNavGroup>
<EuiCollapsibleNavGroup
data-test-subj="collapsibleNavGroup-security"
data-test-subj="collapsibleNavGroup-securitySolution"
iconType="logoSecurity"
initialIsOpen={true}
isCollapsible={true}
key="security"
key="securitySolution"
onToggle={[Function]}
title="Security"
>
@ -1433,7 +1433,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
</EuiFlexGroup>
}
className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-security"
data-test-subj="collapsibleNavGroup-securitySolution"
id="mockId"
initialIsOpen={true}
onToggle={[Function]}
@ -1441,7 +1441,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
>
<div
className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading"
data-test-subj="collapsibleNavGroup-security"
data-test-subj="collapsibleNavGroup-securitySolution"
onToggle={[Function]}
>
<div

View file

@ -2283,10 +2283,12 @@ exports[`Header renders 2`] = `
data-test-subj="headerGlobalNav"
>
<EuiHeader
id="headerGlobalNav"
position="fixed"
>
<div
className="euiHeader euiHeader--default euiHeader--fixed"
id="headerGlobalNav"
>
<EuiHeaderSection
grow={false}
@ -6494,10 +6496,12 @@ exports[`Header renders 3`] = `
data-test-subj="headerGlobalNav"
>
<EuiHeader
id="headerGlobalNav"
position="fixed"
>
<div
className="euiHeader euiHeader--default euiHeader--fixed"
id="headerGlobalNav"
>
<EuiHeaderSection
grow={false}
@ -11306,10 +11310,12 @@ exports[`Header renders 4`] = `
data-test-subj="headerGlobalNav"
>
<EuiHeader
id="headerGlobalNav"
position="fixed"
>
<div
className="euiHeader euiHeader--default euiHeader--fixed"
id="headerGlobalNav"
>
<EuiHeaderSection
grow={false}

View file

@ -125,7 +125,7 @@ export function Header({
<>
<LoadingIndicator loadingCount$={observables.loadingCount$} />
<header className={className} data-test-subj="headerGlobalNav">
<EuiHeader position="fixed">
<EuiHeader position="fixed" id="headerGlobalNav">
<EuiHeaderSection grow={false}>
{navType === 'modern' ? (
<EuiHeaderSectionItem border="right" className="header__toggleNavButtonSection">

View file

@ -3,5 +3,5 @@
}
.kbnGlobalBannerList__item + .kbnGlobalBannerList__item {
margin-top: $euiSize;
margin-top: $euiSizeS;
}

View file

@ -46,7 +46,7 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({
order: 3000,
},
security: {
id: 'security',
id: 'securitySolution',
label: i18n.translate('core.ui.securityNavList.label', {
defaultMessage: 'Security',
}),

View file

@ -4,5 +4,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["management"],
"requiredBundles": ["kibanaReact"]
"optionalPlugins": ["home"],
"requiredBundles": ["kibanaReact", "home"]
}

View file

@ -114,6 +114,21 @@ export class AdvancedSettingsComponent extends Component<
filteredSettings: this.mapSettings(Query.execute(query, this.settings)),
});
});
// scrolls to setting provided in the URL hash
const { hash } = window.location;
if (hash !== '') {
setTimeout(() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
const globalNavOffset = document.getElementById('headerGlobalNav')?.offsetHeight || 0;
if (element) {
element.scrollIntoView();
window.scrollBy(0, -globalNavOffset); // offsets scroll by height of the global nav
}
}, 0);
}
}
componentWillUnmount() {

View file

@ -15,6 +15,7 @@ exports[`Field for array setting should render as read only if saving is disable
</React.Fragment>
}
fullWidth={true}
id="array:test:setting"
title={
<h3>
<span
@ -87,6 +88,7 @@ exports[`Field for array setting should render as read only with help text if ov
</React.Fragment>
}
fullWidth={true}
id="array:test:setting"
title={
<h3>
<span
@ -147,6 +149,7 @@ exports[`Field for array setting should render custom setting icon if it is cust
</React.Fragment>
}
fullWidth={true}
id="array:test:setting"
title={
<h3>
<span
@ -208,6 +211,7 @@ exports[`Field for array setting should render default value if there is no user
</React.Fragment>
}
fullWidth={true}
id="array:test:setting"
title={
<h3>
<span
@ -258,6 +262,7 @@ exports[`Field for array setting should render unsaved value if there are unsave
</React.Fragment>
}
fullWidth={true}
id="array:test:setting"
title={
<h3>
<span
@ -359,6 +364,7 @@ exports[`Field for array setting should render user value if there is user value
</React.Fragment>
}
fullWidth={true}
id="array:test:setting"
title={
<h3>
<span
@ -426,6 +432,7 @@ exports[`Field for boolean setting should render as read only if saving is disab
</React.Fragment>
}
fullWidth={true}
id="boolean:test:setting"
title={
<h3>
<span
@ -504,6 +511,7 @@ exports[`Field for boolean setting should render as read only with help text if
</React.Fragment>
}
fullWidth={true}
id="boolean:test:setting"
title={
<h3>
<span
@ -570,6 +578,7 @@ exports[`Field for boolean setting should render custom setting icon if it is cu
</React.Fragment>
}
fullWidth={true}
id="boolean:test:setting"
title={
<h3>
<span
@ -637,6 +646,7 @@ exports[`Field for boolean setting should render default value if there is no us
</React.Fragment>
}
fullWidth={true}
id="boolean:test:setting"
title={
<h3>
<span
@ -693,6 +703,7 @@ exports[`Field for boolean setting should render unsaved value if there are unsa
</React.Fragment>
}
fullWidth={true}
id="boolean:test:setting"
title={
<h3>
<span
@ -796,6 +807,7 @@ exports[`Field for boolean setting should render user value if there is user val
</React.Fragment>
}
fullWidth={true}
id="boolean:test:setting"
title={
<h3>
<span
@ -869,6 +881,7 @@ exports[`Field for image setting should render as read only if saving is disable
</React.Fragment>
}
fullWidth={true}
id="image:test:setting"
title={
<h3>
<span
@ -943,6 +956,7 @@ exports[`Field for image setting should render as read only with help text if ov
</React.Fragment>
}
fullWidth={true}
id="image:test:setting"
title={
<h3>
<span
@ -1001,6 +1015,7 @@ exports[`Field for image setting should render custom setting icon if it is cust
</React.Fragment>
}
fullWidth={true}
id="image:test:setting"
title={
<h3>
<span
@ -1064,6 +1079,7 @@ exports[`Field for image setting should render default value if there is no user
</React.Fragment>
}
fullWidth={true}
id="image:test:setting"
title={
<h3>
<span
@ -1116,6 +1132,7 @@ exports[`Field for image setting should render unsaved value if there are unsave
</React.Fragment>
}
fullWidth={true}
id="image:test:setting"
title={
<h3>
<span
@ -1214,6 +1231,7 @@ exports[`Field for image setting should render user value if there is user value
</React.Fragment>
}
fullWidth={true}
id="image:test:setting"
title={
<h3>
<span
@ -1317,6 +1335,7 @@ exports[`Field for json setting should render as read only if saving is disabled
</React.Fragment>
}
fullWidth={true}
id="json:test:setting"
title={
<h3>
<span
@ -1412,6 +1431,7 @@ exports[`Field for json setting should render as read only with help text if ove
</React.Fragment>
}
fullWidth={true}
id="json:test:setting"
title={
<h3>
<span
@ -1492,6 +1512,7 @@ exports[`Field for json setting should render custom setting icon if it is custo
</React.Fragment>
}
fullWidth={true}
id="json:test:setting"
title={
<h3>
<span
@ -1598,6 +1619,7 @@ exports[`Field for json setting should render default value if there is no user
</React.Fragment>
}
fullWidth={true}
id="json:test:setting"
title={
<h3>
<span
@ -1685,6 +1707,7 @@ exports[`Field for json setting should render unsaved value if there are unsaved
</React.Fragment>
}
fullWidth={true}
id="json:test:setting"
title={
<h3>
<span
@ -1809,6 +1832,7 @@ exports[`Field for json setting should render user value if there is user value
</React.Fragment>
}
fullWidth={true}
id="json:test:setting"
title={
<h3>
<span
@ -1896,6 +1920,7 @@ exports[`Field for markdown setting should render as read only if saving is disa
</React.Fragment>
}
fullWidth={true}
id="markdown:test:setting"
title={
<h3>
<span
@ -1988,6 +2013,7 @@ exports[`Field for markdown setting should render as read only with help text if
</React.Fragment>
}
fullWidth={true}
id="markdown:test:setting"
title={
<h3>
<span
@ -2068,6 +2094,7 @@ exports[`Field for markdown setting should render custom setting icon if it is c
</React.Fragment>
}
fullWidth={true}
id="markdown:test:setting"
title={
<h3>
<span
@ -2149,6 +2176,7 @@ exports[`Field for markdown setting should render default value if there is no u
</React.Fragment>
}
fullWidth={true}
id="markdown:test:setting"
title={
<h3>
<span
@ -2219,6 +2247,7 @@ exports[`Field for markdown setting should render unsaved value if there are uns
</React.Fragment>
}
fullWidth={true}
id="markdown:test:setting"
title={
<h3>
<span
@ -2336,6 +2365,7 @@ exports[`Field for markdown setting should render user value if there is user va
</React.Fragment>
}
fullWidth={true}
id="markdown:test:setting"
title={
<h3>
<span
@ -2423,6 +2453,7 @@ exports[`Field for number setting should render as read only if saving is disabl
</React.Fragment>
}
fullWidth={true}
id="number:test:setting"
title={
<h3>
<span
@ -2495,6 +2526,7 @@ exports[`Field for number setting should render as read only with help text if o
</React.Fragment>
}
fullWidth={true}
id="number:test:setting"
title={
<h3>
<span
@ -2555,6 +2587,7 @@ exports[`Field for number setting should render custom setting icon if it is cus
</React.Fragment>
}
fullWidth={true}
id="number:test:setting"
title={
<h3>
<span
@ -2616,6 +2649,7 @@ exports[`Field for number setting should render default value if there is no use
</React.Fragment>
}
fullWidth={true}
id="number:test:setting"
title={
<h3>
<span
@ -2666,6 +2700,7 @@ exports[`Field for number setting should render unsaved value if there are unsav
</React.Fragment>
}
fullWidth={true}
id="number:test:setting"
title={
<h3>
<span
@ -2763,6 +2798,7 @@ exports[`Field for number setting should render user value if there is user valu
</React.Fragment>
}
fullWidth={true}
id="number:test:setting"
title={
<h3>
<span
@ -2830,6 +2866,7 @@ exports[`Field for select setting should render as read only if saving is disabl
</React.Fragment>
}
fullWidth={true}
id="select:test:setting"
title={
<h3>
<span
@ -2918,6 +2955,7 @@ exports[`Field for select setting should render as read only with help text if o
</React.Fragment>
}
fullWidth={true}
id="select:test:setting"
title={
<h3>
<span
@ -2994,6 +3032,7 @@ exports[`Field for select setting should render custom setting icon if it is cus
</React.Fragment>
}
fullWidth={true}
id="select:test:setting"
title={
<h3>
<span
@ -3071,6 +3110,7 @@ exports[`Field for select setting should render default value if there is no use
</React.Fragment>
}
fullWidth={true}
id="select:test:setting"
title={
<h3>
<span
@ -3137,6 +3177,7 @@ exports[`Field for select setting should render unsaved value if there are unsav
</React.Fragment>
}
fullWidth={true}
id="select:test:setting"
title={
<h3>
<span
@ -3250,6 +3291,7 @@ exports[`Field for select setting should render user value if there is user valu
</React.Fragment>
}
fullWidth={true}
id="select:test:setting"
title={
<h3>
<span
@ -3333,6 +3375,7 @@ exports[`Field for string setting should render as read only if saving is disabl
</React.Fragment>
}
fullWidth={true}
id="string:test:setting"
title={
<h3>
<span
@ -3405,6 +3448,7 @@ exports[`Field for string setting should render as read only with help text if o
</React.Fragment>
}
fullWidth={true}
id="string:test:setting"
title={
<h3>
<span
@ -3465,6 +3509,7 @@ exports[`Field for string setting should render custom setting icon if it is cus
</React.Fragment>
}
fullWidth={true}
id="string:test:setting"
title={
<h3>
<span
@ -3526,6 +3571,7 @@ exports[`Field for string setting should render default value if there is no use
</React.Fragment>
}
fullWidth={true}
id="string:test:setting"
title={
<h3>
<span
@ -3576,6 +3622,7 @@ exports[`Field for string setting should render unsaved value if there are unsav
</React.Fragment>
}
fullWidth={true}
id="string:test:setting"
title={
<h3>
<span
@ -3673,6 +3720,7 @@ exports[`Field for string setting should render user value if there is user valu
</React.Fragment>
}
fullWidth={true}
id="string:test:setting"
title={
<h3>
<span
@ -3740,6 +3788,7 @@ exports[`Field for stringWithValidation setting should render as read only if sa
</React.Fragment>
}
fullWidth={true}
id="string:test-validation:setting"
title={
<h3>
<span
@ -3812,6 +3861,7 @@ exports[`Field for stringWithValidation setting should render as read only with
</React.Fragment>
}
fullWidth={true}
id="string:test-validation:setting"
title={
<h3>
<span
@ -3872,6 +3922,7 @@ exports[`Field for stringWithValidation setting should render custom setting ico
</React.Fragment>
}
fullWidth={true}
id="string:test-validation:setting"
title={
<h3>
<span
@ -3933,6 +3984,7 @@ exports[`Field for stringWithValidation setting should render default value if t
</React.Fragment>
}
fullWidth={true}
id="string:test-validation:setting"
title={
<h3>
<span
@ -3983,6 +4035,7 @@ exports[`Field for stringWithValidation setting should render unsaved value if t
</React.Fragment>
}
fullWidth={true}
id="string:test-validation:setting"
title={
<h3>
<span
@ -4080,6 +4133,7 @@ exports[`Field for stringWithValidation setting should render user value if ther
</React.Fragment>
}
fullWidth={true}
id="string:test-validation:setting"
title={
<h3>
<span

View file

@ -673,6 +673,7 @@ export class Field extends PureComponent<FieldProps> {
return (
<EuiDescribedFormGroup
id={id}
className={className}
title={this.renderTitle(setting)}
description={this.renderDescription(setting)}

View file

@ -18,6 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin } from 'kibana/public';
import { FeatureCatalogueCategory } from '../../home/public';
import { ComponentRegistry } from './component_registry';
import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types';
@ -29,7 +30,7 @@ const title = i18n.translate('advancedSettings.advancedSettingsLabel', {
export class AdvancedSettingsPlugin
implements Plugin<AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup> {
public setup(core: CoreSetup, { management }: AdvancedSettingsPluginSetup) {
public setup(core: CoreSetup, { management, home }: AdvancedSettingsPluginSetup) {
const kibanaSection = management.sections.section.kibana;
kibanaSection.registerApp({
@ -44,6 +45,21 @@ export class AdvancedSettingsPlugin
},
});
if (home) {
home.featureCatalogue.register({
id: 'advanced_settings',
title,
description: i18n.translate('advancedSettings.featureCatalogueTitle', {
defaultMessage:
'Customize your Kibana experience — change the date format, turn on dark mode, and more.',
}),
icon: 'gear',
path: '/app/management/kibana/settings',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
}
return {
component: component.setup,
};

View file

@ -18,6 +18,8 @@
*/
import { ComponentRegistry } from './component_registry';
import { HomePublicPluginSetup } from '../../home/public';
import { ManagementSetup } from '../../management/public';
export interface AdvancedSettingsSetup {
@ -29,6 +31,7 @@ export interface AdvancedSettingsStart {
export interface AdvancedSettingsPluginSetup {
management: ManagementSetup;
home?: HomePublicPluginSetup;
}
export { ComponentRegistry };

View file

@ -3,7 +3,7 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["devTools", "home"],
"optionalPlugins": ["usageCollection"],
"requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils"]
"requiredPlugins": ["devTools"],
"optionalPlugins": ["usageCollection", "home"],
"requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"]
}

View file

@ -28,19 +28,21 @@ export class ConsoleUIPlugin implements Plugin<void, void, AppSetupUIPluginDepen
{ notifications, getStartServices, http }: CoreSetup,
{ devTools, home, usageCollection }: AppSetupUIPluginDependencies
) {
home.featureCatalogue.register({
id: 'console',
title: i18n.translate('console.devToolsTitle', {
defaultMessage: 'Console',
}),
description: i18n.translate('console.devToolsDescription', {
defaultMessage: 'Skip cURL and use this JSON interface to work with your data directly.',
}),
icon: 'consoleApp',
path: '/app/dev_tools#/console',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN,
});
if (home) {
home.featureCatalogue.register({
id: 'console',
title: i18n.translate('console.devToolsTitle', {
defaultMessage: 'Interact with the Elasticsearch API',
}),
description: i18n.translate('console.devToolsDescription', {
defaultMessage: 'Skip cURL and use a JSON interface to work with your data in Console.',
}),
icon: 'consoleApp',
path: '/app/dev_tools#/console',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
}
devTools.register({
id: 'console',

View file

@ -21,7 +21,7 @@ import { DevToolsSetup } from '../../../dev_tools/public';
import { UsageCollectionSetup } from '../../../usage_collection/public';
export interface AppSetupUIPluginDependencies {
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
devTools: DevToolsSetup;
usageCollection?: UsageCollectionSetup;
}

View file

@ -384,7 +384,7 @@ export class DashboardPlugin
}),
icon: 'dashboardApp',
path: `/app/dashboards#${DashboardConstants.LANDING_PAGE_PATH}`,
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
});
}

View file

@ -30,7 +30,7 @@ export function registerFeature(home: HomePublicPluginSetup) {
}),
icon: 'discoverApp',
path: '/app/discover#/',
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
});
}

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
export const PLUGIN_ID = 'home';
export const HOME_APP_BASE_PATH = `/app/${PLUGIN_ID}`;

View file

@ -35,15 +35,21 @@ export const renderApp = async (
) => {
const homeTitle = i18n.translate('home.breadcrumbs.homeTitle', { defaultMessage: 'Home' });
const { featureCatalogue, chrome } = getServices();
const navLinks = chrome.navLinks.getAll();
// all the directories could be get in "start" phase of plugin after all of the legacy plugins will be moved to a NP
const directories = featureCatalogue.get();
// Filters solutions by available nav links
const solutions = featureCatalogue
.getSolutions()
.filter(({ id }) => navLinks.find(({ category, hidden }) => !hidden && category?.id === id));
chrome.setBreadcrumbs([{ text: homeTitle }]);
render(
<KibanaContextProvider services={{ ...coreStart }}>
<HomeApp directories={directories} />
<HomeApp directories={directories} solutions={solutions} />
</KibanaContextProvider>,
element
);

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ exports[`props iconType 1`] = `
<EuiCard
betaBadgeLabel={null}
className="homSynopsis__card homSynopsis__card--noPanel"
data-test-subj="homeSynopsisLinkgreat tutorial"
data-test-subj="homeSynopsisLinktutorial"
description="this is a great tutorial about..."
href="link_to_item"
icon={
@ -16,6 +16,7 @@ exports[`props iconType 1`] = `
}
layout="horizontal"
title="Great tutorial"
titleElement="h3"
titleSize="xs"
/>
`;
@ -24,7 +25,7 @@ exports[`props iconUrl 1`] = `
<EuiCard
betaBadgeLabel={null}
className="homSynopsis__card homSynopsis__card--noPanel"
data-test-subj="homeSynopsisLinkgreat tutorial"
data-test-subj="homeSynopsisLinktutorial"
description="this is a great tutorial about..."
href="link_to_item"
icon={
@ -36,6 +37,7 @@ exports[`props iconUrl 1`] = `
}
layout="horizontal"
title="Great tutorial"
titleElement="h3"
titleSize="xs"
/>
`;
@ -44,11 +46,12 @@ exports[`props isBeta 1`] = `
<EuiCard
betaBadgeLabel="Beta"
className="homSynopsis__card homSynopsis__card--noPanel"
data-test-subj="homeSynopsisLinkgreat tutorial"
data-test-subj="homeSynopsisLinktutorial"
description="this is a great tutorial about..."
href="link_to_item"
layout="horizontal"
title="Great tutorial"
titleElement="h3"
titleSize="xs"
/>
`;
@ -57,11 +60,12 @@ exports[`render 1`] = `
<EuiCard
betaBadgeLabel={null}
className="homSynopsis__card homSynopsis__card--noPanel"
data-test-subj="homeSynopsisLinkgreat tutorial"
data-test-subj="homeSynopsisLinktutorial"
description="this is a great tutorial about..."
href="link_to_item"
layout="horizontal"
title="Great tutorial"
titleElement="h3"
titleSize="xs"
/>
`;

View file

@ -1,63 +1,22 @@
.homAddData__card {
border: none;
box-shadow: none;
}
.homAddData__cardDivider {
position: relative;
&:after {
position: absolute;
content: '';
width: 1px;
right: -$euiSizeS;
top: 0;
bottom: 0;
background: $euiBorderColor;
}
}
.homAddData__icon {
width: $euiSizeXL * 2;
height: $euiSizeXL * 2;
}
.homAddData__footerItem--highlight {
background-color: tintOrShade($euiColorPrimary, 90%, 70%);
padding: $euiSize;
}
.homAddData__footerItem {
text-align: center;
}
.homAddData__logo {
margin-left: $euiSize;
}
@include euiBreakpoint('xs', 's') {
.homeAddData__flexGroup {
flex-wrap: wrap;
}
}
@include euiBreakpoint('xs', 's', 'm') {
.homAddDat__flexTablet {
flex-direction: column;
}
.homAddData__cardDivider:after {
display: none;
}
.homAddData__cardDivider {
flex-grow: 0 !important;
flex-basis: 100% !important;
}
}
@include euiBreakpoint('l', 'xl') {
.homeAddData__flexGroup {
flex-wrap: nowrap;
}
/*
* 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.
*/
.homDataAdd__content .euiIcon__fillSecondary {
fill: $euiColorDarkestShade;
}

View file

@ -1,5 +1,73 @@
@include euiBreakpoint('xs', 's', 'm') {
.homHome__synopsisItem {
flex-basis: 100% !important;
/*
* 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.
*/
// Local page variables
$homePageWidth: 1200px;
.homWrapper {
background-color: $euiColorEmptyShade;
display: flex;
flex-direction: column;
min-height: calc(100vh - #{$euiHeaderHeightCompensation});
}
.homHeader {
background-color: $euiPageBackgroundColor;
border-bottom: $euiBorderWidthThin solid $euiColorLightShade;
}
.homHeader__inner {
margin: 0 auto;
max-width: $homePageWidth;
padding: $euiSizeXL $euiSize;
.homHeader--hasSolutions & {
padding-bottom: $euiSizeXL + $euiSizeL;
}
}
#homHeader__title {
@include euiBreakpoint('xs', 's') {
text-align: center;
}
}
.homHeader__actionItem {
@include euiBreakpoint('xs', 's') {
margin-bottom: 0 !important;
margin-top: 0 !important;
}
}
.homContent {
margin: 0 auto;
max-width: $homePageWidth;
padding: $euiSizeXL $euiSize;
width: 100%;
}
.homData--expanded {
flex-direction: column;
&,
& > * {
margin-bottom: 0 !important;
margin-top: 0 !important;
}
}

View file

@ -5,9 +5,11 @@
// homChart__legend--small
// homChart__legend-isLoading
@import 'add_data';
@import 'home';
@import 'add_data';
@import 'manage_data';
@import 'sample_data_set_cards';
@import 'solutions_section';
@import 'synopsis';
@import 'welcome';

View file

@ -0,0 +1,22 @@
/*
* 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.
*/
.homDataManage__content .euiIcon__fillSecondary {
fill: $euiColorDarkestShade;
}

View file

@ -0,0 +1,122 @@
/*
* 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.
*/
.homSolutions {
margin-top: -($euiSizeXL + $euiSizeL + $euiSizeM);
}
.homSolutions__content {
min-height: $euiSize * 16;
@include euiBreakpoint('xs', 's') {
flex-direction: column;
}
}
.homSolutions__group {
max-width: 50%;
@include euiBreakpoint('xs', 's') {
max-width: none;
}
}
.homSolutionPanel {
border-radius: $euiBorderRadius;
color: inherit;
flex: 1;
transition: all $euiAnimSpeedFast $euiAnimSlightResistance;
&:hover,
&:focus {
@include euiSlightShadowHover;
transform: translateY(-2px);
.euiTitle {
text-decoration: underline;
}
}
&,
.euiPanel {
display: flex;
flex-direction: column;
}
.euiPanel {
overflow: hidden;
}
}
.homSolutionPanel__header {
color: $euiColorEmptyShade;
padding: $euiSize;
}
.homSolutionPanel__icon {
background-color: $euiColorEmptyShade !important;
box-shadow: none !important;
margin: 0 auto $euiSizeS;
padding: $euiSizeS;
}
.homSolutionPanel__subtitle {
margin-top: $euiSizeXS;
}
.homSolutionPanel__content {
flex-direction: column;
justify-content: center;
padding: $euiSize;
@include euiBreakpoint('xs', 's') {
text-align: center;
}
}
.homSolutionPanel__header {
background-color: $euiColorPrimary;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQYAAAFjCAYAAADfDKXVAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAfiSURBVHgB7d1JblRZGobh45SrhJiWGOQ6EENLSKyABTBmOcwRq2EnTKtUdInBuEln/OkMdB3HTTjiNqd5HinA3YBUSq++24UPUkq/J+jE+/fv09HRURrD27dv0+vXr1OLfksAG4QByAgDkBEGICMMQEYYgIwwABlhADLCAGSEAcgIA5ARBiAjDEBGGICMMAAZYQAywgBkhAHICAOQEQZg05/CAGy6FAYgIwzApnNhADLCAGy6EAZgkzAAGVclgIyTj0DGYgAyZ8IADP2ZLAZgw1n8IQzA0Hn8IQzA0EX8IQzAkEMJICMMwDURhcv44DDBlp4+fZrevHmTxvLq1av04cOHRDHO1h8IA1t7/PhxOjo6SjTrdP2BQwlg7ddiEAYgxGXK8/UnwgCEs+EnwgCEk+EnwgCE0+EnwgDEYcTF8AvCAJxufkEYgO+bXxAG6Nu1y5RrwgB9O7npi8IAfTu+6YvCAP3KrkasCQP06/i2bwgD9Ov0tm8IA/QpLlFe3PZNYYA+/bjrm8IA/YmTjqd3/YAwQH+O7/sBYYC+xHmFH/f9kDBAX/7Y5oeEAfqx1VoIwgD9+L7tDwoD9GHrtRCEAfoQ5xYutv1hYYD2PWgtBGGA9m11JWJIGKBtD14LQRigbf9POxAGaNedT1DeRRigTRGEb2lHwgBtetDlyU3CAO35mXY44TgkDNCWy9XrS9qTMEBb9jqEWBMGaEdchbj3TVi2IQzQhr2uQmwSBmjD5zTCIcSaMED94rzCaRqRMEDd4tLkaIcQa8IA9YpDh70vTd5EGKBOcb9CPCA12nmFIWGAOo16snGTMEB94mTjSZqQMEBdIgqjn2zcJAxQj7ircfIoBGGAOsTtzl/TTIQByhe/nXqSy5K3EQYoW0Rhp/dt3MdhAkoVdzV+Slf3LMxKGKBMcU5h1sOHIYcSUJ5FoxAsBijLLPcp3EcYoBxxOXKUd2DalzDA8uLk4sc08nsq7EMYYFnxINRkT0nuShhgOfEgVDwlOfvlyPsIAyyjmPMJNxEGmFccMsRKKOZ8wk2EAeZT7KHDJmGA6UUI4v6EYg8dNgkDTCued4i7GIu66nAfYYBpVLcShoQBxlflShgSBhhP1SthSBhgHNWvhCFhgP00sxKGhAF29OTJkyKfcxiDN2qBHb18+TIOH5qLQhAGICMMQEYYgIwwABlhADLCAGSEAcgIA5ARBiAjDEBGGICMMNCTy0ePHhX/RqwlEAZ6EQ88/e/Zs2dniXt57JrWNfl+CVMTBloW6+BTavTR6CkJA62KlfAtsRNhoDWxEr788zc7EgZaYiWMRBhogZUwMmGgdlbCBISBWlkJExIGamQlTEwYqImVMBNhoBZx5+LXxCyEgdLFXYufV6/TxGyEgZLFSojzCZ6InJkwUCIrYWHCQGmshAIIA6WwEgoiDJTASiiMMLCYw8NDK6FQ3tqNxbx79+5jEoUiCQOLef78uUOHQgkDkBEGICMMQEYYgIwwABlhADLCAGSEAcgIA5ARBiAjDEBGGNjWxYsXL/5IdOFg9fo9wd3+fr+Ey8vLf63+/k8az38PDg5m/RX1q/+G+Pf/O43j++rf/yU1yPsxcBfvl9ApYeA23lWpY8LAJisBYeAaK4G/CQMhQhAr4SRBEgauYhBRsBL4RRj6ZSVwK2Hok5XAnYShL1YCWxGGflgJbE0Y2mcl8GDC0DYrgZ0IQ5usBPYiDO35uXrFE3+zPrVIW4ShHbES4nbm4wR7EoY2WAmMShjqZiUwCWGol5XAZIShPlYCkxOGulgJzEIY6mAlMCthKJ+VwOyEoVxWAosRhjJZCSxKGMpiJVAEYSiHlUAxhGF5VgLFEYZlWQkUSRiWYSVQNGGYn5VA8YRhPlYC1RCGeVgJVEUYpmUlUCVhmI6VQLWEYXxWAtUThnFZCTRBGMZhJdAUYdiflUBzhGF3VgLNEobdnK1en5KVQKOE4eFiJXxL0DBh2F6shC///A1NE4btWAl0RRjuZiXQJWG4nZVAt4QhZyXQPWG4zkqAJAxrVgIMCMPVnYtfE/BLz2GIuxY/r16nCbim1zDESojzCZeJ3sTh4lj/389To3oLg5XQuYODA4eNW+gpDFYCbKmHMFgJ8ECth8FKgB20GgYrAfbQYhisBNhTS2GwEmAkrYTBSoAR1R4GKwEmUHMYrASYSI1hsBJgYrWFwUqAGdQSBisBZlRDGKwEmFnJYbASYCGlhsFKgAWVFgYrAQpQUhisBChECWGwEqAwS4fBSoACLRUGKwEKtkQYrAQo3JxhsBKgEnOFwUqAikwdhghBrISTBFRjyjBEDCIKVgJUZoowOJcAlRs7DM4lQAPGCsPP1etbshKgCfuGIQ4bYiH8SEAzdg1DHCrEQjhODhugOQ8NgyBAB7YNgyBAR+4LgyBAh24Lg6sM0LFhGGIRfE9XdywKAnQswrBeB2fJ4QKQrsLwMQEM/JYANggDkBEGICMMQEYYgIwwABlhADLCAGSEAcgIA5ARBiAjDEBGGICMMAAZYQAywgBkhAHICAOQEQYgIwxARhiAjDAAGWEAMg/9bdf0LX4h0XkCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACq8hdouPfUCk+KHQAAAABJRU5ErkJggg=='),
url('data:image/png;base64,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');
background-repeat: no-repeat;
background-position: top 0 left 0, bottom 0 right 0;
background-size: $euiSizeXL * 4, $euiSizeXL * 6;
.homSolutionPanel--enterpriseSearch & {
background-color: $euiColorSecondary;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAABKCAYAAADJwhY8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAANMSURBVHgBzZp7buIwEMYHKC1QuqsF7Up7Hs4Al+Ff4DCcAonrtPuAPjcL1B/YyA0p2JkZK580SuQm6Y+xx4+xiSqumrG+d7/z/rYx9t+WZdZ2lFhXxq4jngfkP2Ov9qquq8jnm9Zu6eBNgD6TImwsoC80ibY1NIe1sRcSVsPYHfFVN9Yy1jG2pUPbFZEUoJMP+kYHWJakAZ0AemuvrPZZJ10B8jsdHFFKWh70BSe07X1GkUoBCCHib+w1qsq1qzivLkU6JDUgTSaT7m63C4ZMCmjgaDwe4zYYMhmgB+cEyPal95IAFsA5fTGQZ4dbdcAzcO7/9wxk7bMH8AfMTvJDUsO+fG2tSfJwvp5qtdqKPgEMEYABekeBo0IEnNODgTzpI0MBfaFhnwUtAQdlBvA+X1gG0Kmw0y0J57QykE9+ASdIHo3hF29cARMO6uYDhhvFGPwfjG0E4BxPxy/gVPFR8/m8MRwOsTosPa3y9KEtigBCpmoAJwV5jGixjtp8EG3xD8noOASKjiT2V6+Jr5YLFo2hDuvkDfEErqa7EZXx4naxWKyIr9b+e6QkU0U/iBcw+2jWnM08E09NtMMqA0JNNUC0ReInlRraE9bodXBOzaoD1rQBuVku9SpmZ7eSL9wjVa864LbqgOpVzJ4bagNyv7/RBozZgymSOuAN8bRTA7RrFM4+DJRpepBbvZAqIDf3jQmrThXbxCS3i5FdduYksXOAjUp5QJt75npvK75whwwclopd4uvV3YgB2m7lG8nouK0rAojkEcnlZTZ+plUCsDEajfom9SYBB31InXA/is64h+8sl0vKsowGgwExBO/99QvKDkXISHQpFxCz2Wx/nU6nVFIniacyHkTOpGevJ2J48sR7UKgHnceCRoiSnvxVVOj2P6C6ZyjHVKljwb7a54KDKtKT2MgpPDECD/ZJSYGedEdaCqW+437BkzgcdG/zOIVKciTgDOTawL2dezfVmYUiyHV+V6lIyQAhDxJwjyHvoPv4SWmFgAiCg1JnFqLgIO6qK1SIVowS0afjUgCin/tNJZOZ2oCIUlRr6aOlWoDwFjZz2CczpQHhKXjsYv8WKilAeAwBgL0R0ZPCHECAvFirzCFb/5hyknPV7zL4DLH0CVGgAAAAAElFTkSuQmCC'),
url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA+CAYAAACbQR1vAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACBSURBVHgB7dpBDcAwDMVQdyzGH9xojEQjtbIfBEu5fGUx62WPjyEPcgVArgDIFQC5AiBXAOQKgFwBkCsAcgVATh9gsW+4vFIngFwBkCsAcgVArgDIFQC5AiBXAORahJjVm9zpCoBcAZArAHIFQK4AyBUAuQIgVwDkCoBcAZDTB/gBLrgCk8OhuvYAAAAASUVORK5CYII=');
background-position: top $euiSizeS left 0, bottom $euiSizeS right $euiSizeS;
background-size: $euiSize * 1.25, $euiSizeXL;
}
.homSolutionPanel--observability & {
background-color: $euiColorAccent;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADNSURBVHgB7duxCcIAEEDR08IFrGyz/0pZwVYEIcYF/FWKwHtwXP/hyrvMQbZtu+3rPid3Hf4SKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgIFAQKAgUBAoCBYGCQEGgcJmDnt6WZfms6/qak/sFeswx3vs85+ScWBAoCBQECgIFgYJAQaAgUBAoCBQECgIFgYJAQaAgUBAoCBQECgKFLx4KCqcIFEJnAAAAAElFTkSuQmCC');
background-position: top $euiSizeS right $euiSizeS;
background-size: $euiSizeL * 1.5;
}
.homSolutionPanel--securitySolution & {
background-color: $euiColorDarkestShade;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGIAAABiCAYAAACrpQYOAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAKRSURBVHgB7d3tSgMxEIXhMeqPQoWC9f4vte5Z3dLvTbKZzCQ5LyxqKwg+tNjdUxQR+RJmXpiOvRDDvPD/kRjGhYvPgbETZlK4+fogxDApPLiNGAaFJ7cD40NYtcKL+76FGNUKK/cRo1Ih4n5gvAtTbQ1i+R5iKBcDgYBADMViIRAxFEuBQMRQKhUCEUOhHAi0YLwJK1IuBCJGwbZAoE8hRpG2QqAFg22oBAQCxkFYdqUgEE6dEyOzkhCIGJmVhkDA4PXvxDQgEMcIiWlBIGIkpAmBiBGZNgQiRkQ1IBAxVqoFgThge1FNCMTN1JNqQyBiPMgCAnHAdpMVBOJm6iJLCA7YLrKEWH4+r3+LPYQIMeY8QKDhlyFeINDQGJ4g0LAY3iDQkBgeIdBwmymvEGgoDM8QaJgBm3cINMSArQUI1P2ArRUI1PVmqiUI1C1GaxCoywFbixCouzFCqxCIyxBPnU6nLjBafkQs7YExHdJyPUCg+WmqZYxeIBAwmv3TticItJseFYcWHxm9QaD5RV9rGD1CIGAcJ4xmztr2CoHms7atYPQMgc4Y3p+qeodAwPiZjnfPGCNAoPMgwSvGKBDINcZIEMgtxmgQyCXGiBDIHcaoEAgYx+n48IAxMgQ6v1nGGmN0COQCgxB/4feAF307KwxCXDe/9dgCgxD3mWAQ4nHAqHrplRDPq3odnBCvq4ZBiPWqYBAiLnUMQsSnikGItObtlAYGIdJTmesQIq/iGITIr+h2ihDbKradIsT2imynCFGmzdspQpRr03VwQpQtG4MQ5cvCIIROyRiE0CsJgxC6RW+nCKFf1FyHEHVaxSBEvV5upwhRv4dzHULYdIdBCLuutlOEsO18HZz/u8E+YMgvrbKfmp8y7IEAAAAASUVORK5CYII=');
background-position: top $euiSizeS left $euiSizeS;
background-size: $euiSizeL * 2;
}
}

View file

@ -5,6 +5,10 @@
box-shadow: none;
}
.homSynopsis__cardTitle {
display: flex;
}
// SASSTODO: Fix in EUI
.euiCard__content {
padding-top: 0 !important;

View file

@ -1,320 +0,0 @@
/*
* 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.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { getServices } from '../kibana_services';
import {
EuiButton,
EuiLink,
EuiPanel,
EuiTitle,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiCard,
EuiIcon,
EuiHorizontalRule,
EuiFlexGrid,
} from '@elastic/eui';
const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => {
const basePath = getServices().getBasePath();
const renderCards = () => {
const apmData = {
title: intl.formatMessage({
id: 'home.addData.apm.nameTitle',
defaultMessage: 'APM',
}),
description: intl.formatMessage({
id: 'home.addData.apm.nameDescription',
defaultMessage:
'APM automatically collects in-depth performance metrics and errors from inside your applications.',
}),
ariaDescribedby: 'aria-describedby.addAmpButtonLabel',
};
const loggingData = {
title: intl.formatMessage({
id: 'home.addData.logging.nameTitle',
defaultMessage: 'Logs',
}),
description: intl.formatMessage({
id: 'home.addData.logging.nameDescription',
defaultMessage:
'Ingest logs from popular data sources and easily visualize in preconfigured dashboards.',
}),
ariaDescribedby: 'aria-describedby.addLogDataButtonLabel',
};
const metricsData = {
title: intl.formatMessage({
id: 'home.addData.metrics.nameTitle',
defaultMessage: 'Metrics',
}),
description: intl.formatMessage({
id: 'home.addData.metrics.nameDescription',
defaultMessage:
'Collect metrics from the operating system and services running on your servers.',
}),
ariaDescribedby: 'aria-describedby.addMetricsButtonLabel',
};
const siemData = {
title: intl.formatMessage({
id: 'home.addData.securitySolution.nameTitle',
defaultMessage: 'SIEM + Endpoint Security',
}),
description: intl.formatMessage({
id: 'home.addData.securitySolution.nameDescription',
defaultMessage:
'Protect hosts, analyze security information and events, hunt threats, automate detections, and create cases.',
}),
ariaDescribedby: 'aria-describedby.addSiemButtonLabel',
};
const getApmCard = () => (
<EuiFlexItem grow={1} className="homAddData__flexItem">
<EuiCard
textAlign="left"
className="homAddData__card"
titleSize="xs"
title={apmData.title}
description={<span id={apmData.ariaDescribedby}>{apmData.description}</span>}
footer={
<EuiButton
className="homAddData__button"
href="#/tutorial/apm"
aria-describedby={apmData.ariaDescribedby}
>
<FormattedMessage id="home.addData.apm.addApmButtonLabel" defaultMessage="Add APM" />
</EuiButton>
}
/>
</EuiFlexItem>
);
return (
<EuiFlexGroup
className="homeAddData__flexGroup homAddData__flexTablet"
wrap={apmUiEnabled}
gutterSize="l"
justifyContent="spaceAround"
responsive={false}
>
<EuiFlexItem className="homAddData__cardDivider homAddData__flexItem" grow={3}>
<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon size="xl" type="logoObservability" className="homAddData__logo" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>
<FormattedMessage
id="home.addData.title.observability"
defaultMessage="Observability"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup
className="homeAddData__flexGroup"
wrap={apmUiEnabled}
gutterSize="l"
justifyContent="spaceAround"
responsive={false}
>
{apmUiEnabled !== false && getApmCard()}
<EuiFlexItem grow={1} className="homAddData__flexItem">
<EuiCard
textAlign="left"
className="homAddData__card"
title={loggingData.title}
titleSize="xs"
description={
<span id={loggingData.ariaDescribedby}>{loggingData.description}</span>
}
footer={
<EuiButton
className="homAddData__button"
data-test-subj="logsData"
href="#/tutorial_directory/logging"
aria-describedby={loggingData.ariaDescribedby}
>
<FormattedMessage
id="home.addData.logging.addLogDataButtonLabel"
defaultMessage="Add log data"
/>
</EuiButton>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={1} className="homAddData__flexItem">
<EuiCard
textAlign="left"
className="homAddData__card"
title={metricsData.title}
titleSize="xs"
description={
<span id={metricsData.ariaDescribedby}>{metricsData.description}</span>
}
footer={
<EuiButton
className="homAddData__button"
href="#/tutorial_directory/metrics"
aria-describedby={metricsData.ariaDescribedby}
>
<FormattedMessage
id="home.addData.metrics.addMetricsDataButtonLabel"
defaultMessage="Add metric data"
/>
</EuiButton>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={1} className="homAddData__flexItem">
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon size="xl" type="logoSecurity" className="homAddData__logo" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>
<FormattedMessage id="home.addData.title.security" defaultMessage="Security" />
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiCard
textAlign="left"
titleSize="xs"
className="homAddData__card"
title={siemData.title}
description={<span id={siemData.ariaDescribedby}>{siemData.description}</span>}
footer={
<EuiButton
className="homAddData__button"
href="#/tutorial_directory/security"
aria-describedby={siemData.ariaDescribedby}
>
<FormattedMessage
id="home.addData.securitySolution.addSecurityEventsButtonLabel"
defaultMessage="Add events"
/>
</EuiButton>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
const footerItemClasses = classNames('homAddData__footerItem', {
'homAddData__footerItem--highlight': isNewKibanaInstance,
});
return (
<EuiPanel paddingSize="l">
{renderCards()}
<EuiHorizontalRule />
<EuiFlexGrid columns={mlEnabled !== false ? 3 : 2}>
<EuiFlexItem className={footerItemClasses}>
<EuiText size="s">
<strong style={{ height: 38 }}>
<FormattedMessage
id="home.addData.sampleDataTitle"
defaultMessage="Add sample data"
/>
</strong>
<EuiLink
style={{ display: 'block', textAlign: 'center' }}
href="#/tutorial_directory/sampleData"
>
<FormattedMessage
id="home.addData.sampleDataLink"
defaultMessage="Load a data set and a Kibana dashboard"
/>
</EuiLink>
</EuiText>
</EuiFlexItem>
{mlEnabled !== false ? (
<EuiFlexItem className={footerItemClasses}>
<EuiText size="s">
<strong style={{ height: 38 }}>
<FormattedMessage
id="home.addData.uploadFileTitle"
defaultMessage="Upload data from log file"
/>
</strong>
<EuiLink
style={{ display: 'block', textAlign: 'center' }}
href={`${basePath}/app/ml#/filedatavisualizer`}
>
<FormattedMessage
id="home.addData.uploadFileLink"
defaultMessage="Import a CSV, NDJSON, or log file"
/>
</EuiLink>
</EuiText>
</EuiFlexItem>
) : null}
<EuiFlexItem className={footerItemClasses}>
<EuiText size="s">
<strong style={{ height: 38 }}>
<FormattedMessage
id="home.addData.yourDataTitle"
defaultMessage="Use Elasticsearch data"
/>
</strong>
<EuiLink
style={{ display: 'block', textAlign: 'center' }}
href={`${basePath}/app/management/kibana/indexPatterns`}
>
<FormattedMessage
id="home.addData.yourDataLink"
defaultMessage="Connect to your Elasticsearch index"
/>
</EuiLink>
</EuiText>
</EuiFlexItem>
</EuiFlexGrid>
</EuiPanel>
);
};
AddDataUi.propTypes = {
apmUiEnabled: PropTypes.bool.isRequired,
mlEnabled: PropTypes.bool.isRequired,
isNewKibanaInstance: PropTypes.bool.isRequired,
};
export const AddData = injectI18n(AddDataUi);

View file

@ -1,68 +0,0 @@
/*
* 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.
*/
import React from 'react';
import { AddData } from './add_data';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { getServices } from '../kibana_services';
jest.mock('../kibana_services', () => {
const mock = {
getBasePath: jest.fn(() => 'path'),
};
return {
getServices: () => mock,
};
});
beforeEach(() => {
jest.clearAllMocks();
});
test('render', () => {
const component = shallowWithIntl(
<AddData.WrappedComponent apmUiEnabled={false} mlEnabled={false} isNewKibanaInstance={false} />
);
expect(component).toMatchSnapshot(); // eslint-disable-line
expect(getServices().getBasePath).toHaveBeenCalledTimes(1);
});
test('mlEnabled', () => {
const component = shallowWithIntl(
<AddData.WrappedComponent apmUiEnabled={true} mlEnabled={true} isNewKibanaInstance={false} />
);
expect(component).toMatchSnapshot(); // eslint-disable-line
expect(getServices().getBasePath).toHaveBeenCalledTimes(1);
});
test('apmUiEnabled', () => {
const component = shallowWithIntl(
<AddData.WrappedComponent apmUiEnabled={true} mlEnabled={false} isNewKibanaInstance={false} />
);
expect(component).toMatchSnapshot(); // eslint-disable-line
expect(getServices().getBasePath).toHaveBeenCalledTimes(1);
});
test('isNewKibanaInstance', () => {
const component = shallowWithIntl(
<AddData.WrappedComponent apmUiEnabled={false} mlEnabled={false} isNewKibanaInstance={true} />
);
expect(component).toMatchSnapshot(); // eslint-disable-line
expect(getServices().getBasePath).toHaveBeenCalledTimes(1);
});

View file

@ -0,0 +1,96 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddData render 1`] = `
<section
aria-labelledby="homDataAdd__title"
className="homDataAdd"
>
<EuiFlexGroup
alignItems="center"
responsive={false}
>
<EuiFlexItem
grow={1}
>
<EuiTitle
size="s"
>
<h2
id="homDataAdd__title"
>
<FormattedMessage
defaultMessage="Ingest your data"
id="home.addData.sectionTitle"
values={Object {}}
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
flush="right"
href="#/tutorial_directory/sampleData"
iconType="visTable"
size="xs"
>
<FormattedMessage
defaultMessage="Try our sample data"
id="home.addData.sampleDataButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
className="homDataAdd__content"
>
<EuiFlexItem
key="home_tutorial_directory"
>
<Synopsis
description="Ingest data from popular apps and services."
iconType="indexOpen"
id="home_tutorial_directory"
isBeta={false}
onClick={[Function]}
title="Ingest data"
url="/app/home#/tutorial_directory"
wrapInPanel={true}
/>
</EuiFlexItem>
<EuiFlexItem
key="ingestManager"
>
<Synopsis
description="Add and manage your fleet of Elastic Agents and integrations."
iconType="indexManagementApp"
id="ingestManager"
isBeta={false}
onClick={[Function]}
title="Add Elastic Agent"
url="/app/ingestManager"
wrapInPanel={true}
/>
</EuiFlexItem>
<EuiFlexItem
key="ml_file_data_visualizer"
>
<Synopsis
description="Import your own CSV, NDJSON, or log file"
iconType="document"
id="ml_file_data_visualizer"
isBeta={false}
onClick={[Function]}
title="Upload a file"
url="/app/ml#/filedatavisualizer"
wrapInPanel={true}
/>
</EuiFlexItem>
</EuiFlexGroup>
</section>
`;

View file

@ -0,0 +1,95 @@
/*
* 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.
*/
/*
* 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.
*/
import React from 'react';
import { AddData } from './add_data';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
jest.mock('../app_navigation_handler', () => {
return {
createAppNavigationHandler: jest.fn(() => () => {}),
};
});
beforeEach(() => {
jest.clearAllMocks();
});
const addBasePathMock = jest.fn((path: string) => (path ? path : 'path'));
const mockFeatures = [
{
category: 'data',
description: 'Ingest data from popular apps and services.',
homePageSection: 'add_data',
icon: 'indexOpen',
id: 'home_tutorial_directory',
order: 500,
path: '/app/home#/tutorial_directory',
title: 'Ingest data',
},
{
category: 'admin',
description: 'Add and manage your fleet of Elastic Agents and integrations.',
homePageSection: 'add_data',
icon: 'indexManagementApp',
id: 'ingestManager',
order: 510,
path: '/app/ingestManager',
title: 'Add Elastic Agent',
},
{
category: 'data',
description: 'Import your own CSV, NDJSON, or log file',
homePageSection: 'add_data',
icon: 'document',
id: 'ml_file_data_visualizer',
order: 520,
path: '/app/ml#/filedatavisualizer',
title: 'Upload a file',
},
];
describe('AddData', () => {
test('render', () => {
const component = shallowWithIntl(
<AddData addBasePath={addBasePathMock} features={mockFeatures} />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,95 @@
/*
* 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.
*/
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
// @ts-expect-error untyped service
import { FeatureCatalogueEntry } from '../../services';
import { createAppNavigationHandler } from '../app_navigation_handler';
// @ts-expect-error untyped component
import { Synopsis } from '../synopsis';
interface Props {
addBasePath: (path: string) => string;
features: FeatureCatalogueEntry[];
}
export const AddData: FC<Props> = ({ addBasePath, features }) => (
<section className="homDataAdd" aria-labelledby="homDataAdd__title">
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem grow={1}>
<EuiTitle size="s">
<h2 id="homDataAdd__title">
<FormattedMessage id="home.addData.sectionTitle" defaultMessage="Ingest your data" />
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="visTable"
href="#/tutorial_directory/sampleData"
size="xs"
flush="right"
>
<FormattedMessage
id="home.addData.sampleDataButtonLabel"
defaultMessage="Try our sample data"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup className="homDataAdd__content">
{features.map((feature) => (
<EuiFlexItem key={feature.id}>
<Synopsis
id={feature.id}
onClick={createAppNavigationHandler(feature.path)}
description={feature.description}
iconType={feature.icon}
title={feature.title}
url={addBasePath(feature.path)}
wrapInPanel
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</section>
);
AddData.propTypes = {
addBasePath: PropTypes.func.isRequired,
features: 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,
order: PropTypes.number,
})
),
};

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export * from './add_data';

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { MouseEvent } from 'react';
import { getServices } from '../kibana_services';
export const createAppNavigationHandler = (targetUrl: string) => (event: MouseEvent) => {

View file

@ -115,6 +115,7 @@ export class FeatureDirectory extends React.Component {
return (
<EuiFlexItem key={directory.id}>
<Synopsis
id={directory.id}
onClick={createAppNavigationHandler(directory.path)}
description={directory.description}
iconType={directory.icon}
@ -157,6 +158,7 @@ FeatureDirectory.propTypes = {
path: PropTypes.string.isRequired,
showOnHomePage: PropTypes.bool.isRequired,
category: PropTypes.string.isRequired,
order: PropTypes.number,
})
),
};

View file

@ -19,29 +19,22 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Synopsis } from './synopsis';
import { AddData } from './add_data';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
EuiPage,
EuiPanel,
EuiButtonEmpty,
EuiTitle,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiFlexGrid,
EuiText,
EuiPageBody,
EuiScreenReaderOnly,
EuiHorizontalRule,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Welcome } from './welcome';
import { getServices } from '../kibana_services';
import { FeatureCatalogueCategory } from '../../services';
import { getServices } from '../kibana_services';
import { AddData } from './add_data';
import { createAppNavigationHandler } from './app_navigation_handler';
import { ManageData } from './manage_data';
import { SolutionsSection } from './solutions_section';
import { Welcome } from './welcome';
const KEY_ENABLE_WELCOME = 'home:welcome:show';
@ -53,6 +46,10 @@ export class Home extends Component {
getServices().homeConfig.disableWelcomeScreen ||
props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false'
);
const body = document.querySelector('body');
body.classList.add('isHomPage');
this.state = {
// If welcome is enabled, we wait for loading to complete
// before rendering. This prevents an annoying flickering
@ -116,105 +113,144 @@ export class Home extends Component {
this._isMounted && this.setState({ isWelcomeEnabled: false });
};
renderDirectories = (category) => {
const { addBasePath, directories } = this.props;
return directories
.filter((directory) => {
return directory.showOnHomePage && directory.category === category;
})
.map((directory) => {
return (
<EuiFlexItem className="homHome__synopsisItem" key={directory.id}>
<Synopsis
onClick={createAppNavigationHandler(directory.path)}
description={directory.description}
iconType={directory.icon}
title={directory.title}
url={addBasePath(directory.path)}
/>
</EuiFlexItem>
);
});
};
findDirectoryById = (id) => this.props.directories.find((directory) => directory.id === id);
getFeaturesByCategory = (category) =>
this.props.directories
.filter((directory) => directory.showOnHomePage && directory.category === category)
.sort((directoryA, directoryB) => directoryA.order - directoryB.order);
renderNormal() {
const { apmUiEnabled, mlEnabled } = this.props;
const { addBasePath, solutions } = this.props;
const devTools = this.findDirectoryById('console');
const stackManagement = this.findDirectoryById('stack-management');
const advancedSettings = this.findDirectoryById('advanced_settings');
const addDataFeatures = this.getFeaturesByCategory(FeatureCatalogueCategory.DATA);
const manageDataFeatures = this.getFeaturesByCategory(FeatureCatalogueCategory.ADMIN);
// Show card for console if none of the manage data plugins are available, most likely in OSS
if (manageDataFeatures.length < 1 && devTools) {
manageDataFeatures.push(devTools);
}
return (
<EuiPage restrictWidth={1200} data-test-subj="homeApp">
<EuiPageBody className="eui-displayBlock">
<EuiScreenReaderOnly>
<h1>
<FormattedMessage id="home.welcomeHomePageHeader" defaultMessage="Kibana home" />
</h1>
</EuiScreenReaderOnly>
<AddData
apmUiEnabled={apmUiEnabled}
mlEnabled={mlEnabled}
isNewKibanaInstance={this.state.isNewKibanaInstance}
/>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiPanel paddingSize="l">
<EuiTitle size="s">
<h2>
<FormattedMessage
id="home.directories.visualize.nameTitle"
defaultMessage="Visualize and Explore Data"
/>
</h2>
<main aria-labelledby="homHeader__title" className="homWrapper" data-test-subj="homeApp">
<header
className={`homHeader ${
solutions.length ? 'homHeader--hasSolutions' : 'homHeader--noSolutions'
}`}
>
<div className="homHeader__inner">
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="m">
<h1 id="homHeader__title">
<FormattedMessage id="home.pageHeader.title" defaultMessage="Home" />
</h1>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexGrid columns={2} gutterSize="s">
{this.renderDirectories(FeatureCatalogueCategory.DATA)}
</EuiFlexGrid>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup className="homHeader__actions">
<EuiFlexItem className="homHeader__actionItem">
<EuiButtonEmpty href="#/tutorial_directory" iconType="indexOpen">
{i18n.translate('home.pageHeader.addDataButtonLabel', {
defaultMessage: 'Add data',
})}
</EuiButtonEmpty>
</EuiFlexItem>
{stackManagement ? (
<EuiFlexItem className="homHeader__actionItem">
<EuiButtonEmpty
onClick={createAppNavigationHandler(stackManagement.path)}
iconType="gear"
>
{i18n.translate('home.pageHeader.stackManagementButtonLabel', {
defaultMessage: 'Manage',
})}
</EuiButtonEmpty>
</EuiFlexItem>
) : null}
{devTools ? (
<EuiFlexItem className="homHeader__actionItem">
<EuiButtonEmpty
onClick={createAppNavigationHandler(devTools.path)}
iconType="wrench"
>
{i18n.translate('home.pageHeader.devToolsButtonLabel', {
defaultMessage: 'Dev tools',
})}
</EuiButtonEmpty>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</header>
<div className="homContent">
{solutions.length && <SolutionsSection addBasePath={addBasePath} solutions={solutions} />}
<EuiFlexGroup
className={`homData ${
addDataFeatures.length === 1 && manageDataFeatures.length === 1
? 'homData--compressed'
: 'homData--expanded'
}`}
>
<EuiFlexItem>
<AddData addBasePath={addBasePath} features={addDataFeatures} />
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel paddingSize="l">
<EuiTitle size="s">
<h2>
<FormattedMessage
id="home.directories.manage.nameTitle"
defaultMessage="Manage and Administer the Elastic Stack"
/>
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexGrid columns={2}>
{this.renderDirectories(FeatureCatalogueCategory.ADMIN)}
</EuiFlexGrid>
</EuiPanel>
<ManageData addBasePath={addBasePath} features={manageDataFeatures} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiHorizontalRule margin="xl" aria-hidden="true" />
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false} className="eui-textCenter">
<EuiText size="s" color="subdued">
<p>
<footer className="homFooter">
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{advancedSettings && (
<EuiButtonEmpty
iconType="home"
onClick={createAppNavigationHandler(
'/app/management/kibana/settings#defaultRoute'
)}
size="xs"
>
<FormattedMessage
id="home.changeHomeRouteLink"
defaultMessage="Display a different page on log in"
/>
</EuiButtonEmpty>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="allPlugins"
href="#/feature_directory"
size="xs"
flush="right"
iconType="apps"
>
<FormattedMessage
id="home.directories.notFound.description"
defaultMessage="Didnt find what you were looking for?"
id="home.appDirectory.appDirectoryButtonLabel"
defaultMessage="View app directory"
/>
</p>
</EuiText>
<EuiSpacer size="s" />
<EuiButton data-test-subj="allPlugins" href="#/feature_directory">
<FormattedMessage
id="home.directories.notFound.viewFullButtonLabel"
defaultMessage="View full directory of Kibana plugins"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</EuiPage>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</footer>
</div>
</main>
);
}
@ -260,13 +296,23 @@ Home.propTypes = {
path: PropTypes.string.isRequired,
showOnHomePage: PropTypes.bool.isRequired,
category: PropTypes.string.isRequired,
order: PropTypes.number,
})
),
solutions: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
descriptions: PropTypes.arrayOf(PropTypes.string).isRequired,
icon: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
order: PropTypes.number,
})
),
apmUiEnabled: PropTypes.bool.isRequired,
find: PropTypes.func.isRequired,
localStorage: PropTypes.object.isRequired,
urlBasePath: PropTypes.string.isRequired,
mlEnabled: PropTypes.bool.isRequired,
telemetry: PropTypes.shape({
telemetryService: PropTypes.any,
telemetryNotifications: PropTypes.any,

View file

@ -41,6 +41,7 @@ describe('home', () => {
beforeEach(() => {
defaultProps = {
directories: [],
solutions: [],
apmUiEnabled: true,
mlEnabled: true,
kibanaVersion: '99.2.1',
@ -92,8 +93,96 @@ describe('home', () => {
expect(component).toMatchSnapshot();
});
describe('header', () => {
test('render', async () => {
const component = await renderHome();
expect(component).toMatchSnapshot();
});
test('should show "Manage" link if stack management is available', async () => {
const directoryEntry = {
id: 'stack-management',
title: 'Management',
description: 'Your center console for managing the Elastic Stack.',
icon: 'managementApp',
path: 'management_landing_page',
category: FeatureCatalogueCategory.ADMIN,
showOnHomePage: false,
};
const component = await renderHome({
directories: [directoryEntry],
});
expect(component).toMatchSnapshot();
});
test('should show "Dev tools" link if console is available', async () => {
const directoryEntry = {
id: 'console',
title: 'Console',
description: 'Skip cURL and use a JSON interface to work with your data in Console.',
icon: 'consoleApp',
path: 'path-to-dev-tools',
category: FeatureCatalogueCategory.ADMIN,
showOnHomePage: false,
};
const component = await renderHome({
directories: [directoryEntry],
});
expect(component).toMatchSnapshot();
});
});
describe('directories', () => {
test('should render DATA directory entry in "Explore Data" panel', async () => {
test('should render solutions in the "solution section"', async () => {
const solutionEntry1 = {
id: 'kibana',
title: 'Kibana',
subtitle: 'Visualize & analyze',
descriptions: ['Analyze data in dashboards'],
icon: 'logoKibana',
path: 'kibana_landing_page',
order: 1,
};
const solutionEntry2 = {
id: 'solution-2',
title: 'Solution two',
subtitle: 'Subtitle for solution two',
descriptions: ['Example use case'],
icon: 'empty',
path: 'path-to-solution-two',
order: 2,
};
const solutionEntry3 = {
id: 'solution-3',
title: 'Solution three',
subtitle: 'Subtitle for solution three',
descriptions: ['Example use case'],
icon: 'empty',
path: 'path-to-solution-three',
order: 3,
};
const solutionEntry4 = {
id: 'solution-4',
title: 'Solution four',
subtitle: 'Subtitle for solution four',
descriptions: ['Example use case'],
icon: 'empty',
path: 'path-to-solution-four',
order: 4,
};
const component = await renderHome({
solutions: [solutionEntry1, solutionEntry2, solutionEntry3, solutionEntry4],
});
expect(component).toMatchSnapshot();
});
test('should render DATA directory entry in "Ingest your data" panel', async () => {
const directoryEntry = {
id: 'dashboard',
title: 'Dashboard',
@ -111,7 +200,7 @@ describe('home', () => {
expect(component).toMatchSnapshot();
});
test('should render ADMIN directory entry in "Manage" panel', async () => {
test('should render ADMIN directory entry in "Manage your data" panel', async () => {
const directoryEntry = {
id: 'index_patterns',
title: 'Index Patterns',
@ -148,6 +237,26 @@ describe('home', () => {
});
});
describe('change home route', () => {
test('should render a link to change the default route in advanced settings if advanced settings is enabled', async () => {
const component = await renderHome({
directories: [
{
description: 'Change your settings',
icon: 'gear',
id: 'advanced_settings',
path: 'path-to-advanced_settings',
showOnHomePage: false,
title: 'Advanced settings',
category: FeatureCatalogueCategory.ADMIN,
},
],
});
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');

View file

@ -38,7 +38,7 @@ const RedirectToDefaultApp = () => {
return null;
};
export function HomeApp({ directories }) {
export function HomeApp({ directories, solutions }) {
const {
savedObjectsClient,
getBasePath,
@ -48,8 +48,6 @@ export function HomeApp({ directories }) {
} = getServices();
const environment = environmentService.getEnvironment();
const isCloudEnabled = environment.cloud;
const mlEnabled = environment.ml;
const apmUiEnabled = environment.apmUi;
const renderTutorialDirectory = (props) => {
return (
@ -87,8 +85,7 @@ export function HomeApp({ directories }) {
<Home
addBasePath={addBasePath}
directories={directories}
apmUiEnabled={apmUiEnabled}
mlEnabled={mlEnabled}
solutions={solutions}
find={savedObjectsClient.find}
localStorage={localStorage}
urlBasePath={getBasePath()}
@ -112,6 +109,18 @@ HomeApp.propTypes = {
path: PropTypes.string.isRequired,
showOnHomePage: PropTypes.bool.isRequired,
category: PropTypes.string.isRequired,
order: PropTypes.number,
})
),
solutions: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
descriptions: PropTypes.arrayOf(PropTypes.string).isRequired,
icon: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
order: PropTypes.number,
})
),
};

View file

@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ManageData render 1`] = `
<Fragment>
<EuiHorizontalRule
aria-hidden="true"
margin="xl"
/>
<section
aria-labelledby="homDataManage__title"
className="homDataManage"
>
<EuiTitle
size="s"
>
<h2
id="homDataManage__title"
>
<FormattedMessage
defaultMessage="Manage your data"
id="home.manageData.sectionTitle"
values={Object {}}
/>
</h2>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
className="homDataManage__content"
>
<EuiFlexItem
key="security"
>
<Synopsis
description="Control who has access and what tasks they can perform."
iconType="securityApp"
id="security"
isBeta={false}
onClick={[Function]}
title="Protect your data"
url="path-to-security-roles"
wrapInPanel={true}
/>
</EuiFlexItem>
<EuiFlexItem
key="monitoring"
>
<Synopsis
description="Track the real-time health and performance of your deployment."
iconType="monitoringApp"
id="monitoring"
isBeta={false}
onClick={[Function]}
title="Monitor the stack"
url="path-to-monitoring"
wrapInPanel={true}
/>
</EuiFlexItem>
<EuiFlexItem
key="snapshot_restore"
>
<Synopsis
description="Save snapshots to a backup repository, and restore to recover index and cluster state."
iconType="storage"
id="snapshot_restore"
isBeta={false}
onClick={[Function]}
title="Store & recover backups"
url="path-to-snapshot-restore"
wrapInPanel={true}
/>
</EuiFlexItem>
<EuiFlexItem
key="index_lifecycle_management"
>
<Synopsis
description="Define lifecycle policies to automatically perform operations as an index ages."
iconType="indexSettings"
id="index_lifecycle_management"
isBeta={false}
onClick={[Function]}
title="Manage index lifecycles"
url="path-to-index-lifecycle-management"
wrapInPanel={true}
/>
</EuiFlexItem>
</EuiFlexGroup>
</section>
</Fragment>
`;

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export * from './manage_data';

View file

@ -0,0 +1,91 @@
/*
* 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.
*/
import React from 'react';
import { ManageData } from './manage_data';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
jest.mock('../app_navigation_handler', () => {
return {
createAppNavigationHandler: jest.fn(() => () => {}),
};
});
beforeEach(() => {
jest.clearAllMocks();
});
const addBasePathMock = jest.fn((path: string) => (path ? path : 'path'));
const mockFeatures = [
{
category: 'admin',
description: 'Control who has access and what tasks they can perform.',
homePageSection: 'manage_data',
icon: 'securityApp',
id: 'security',
order: 600,
path: 'path-to-security-roles',
title: 'Protect your data',
showOnHomePage: true,
},
{
category: 'admin',
description: 'Track the real-time health and performance of your deployment.',
homePageSection: 'manage_data',
icon: 'monitoringApp',
id: 'monitoring',
order: 610,
path: 'path-to-monitoring',
title: 'Monitor the stack',
showOnHomePage: true,
},
{
category: 'admin',
description:
'Save snapshots to a backup repository, and restore to recover index and cluster state.',
homePageSection: 'manage_data',
icon: 'storage',
id: 'snapshot_restore',
order: 630,
path: 'path-to-snapshot-restore',
title: 'Store & recover backups',
showOnHomePage: true,
},
{
category: 'admin',
description: 'Define lifecycle policies to automatically perform operations as an index ages.',
homePageSection: 'manage_data',
icon: 'indexSettings',
id: 'index_lifecycle_management',
order: 640,
path: 'path-to-index-lifecycle-management',
title: 'Manage index lifecycles',
showOnHomePage: true,
},
];
describe('ManageData', () => {
test('render', () => {
const component = shallowWithIntl(
<ManageData addBasePath={addBasePathMock} features={mockFeatures} />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,81 @@
/*
* 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.
*/
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiHorizontalRule, EuiSpacer, EuiTitle, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
// @ts-expect-error untyped service
import { FeatureCatalogueEntry } from '../../services';
import { createAppNavigationHandler } from '../app_navigation_handler';
// @ts-expect-error untyped component
import { Synopsis } from '../synopsis';
interface Props {
addBasePath: (path: string) => string;
features: FeatureCatalogueEntry[];
}
export const ManageData: FC<Props> = ({ addBasePath, features }) => (
<>
{features.length > 1 && <EuiHorizontalRule margin="xl" aria-hidden="true" />}
<section className="homDataManage" aria-labelledby="homDataManage__title">
<EuiTitle size="s">
<h2 id="homDataManage__title">
<FormattedMessage id="home.manageData.sectionTitle" defaultMessage="Manage your data" />
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexGroup className="homDataManage__content">
{features.map((feature) => (
<EuiFlexItem key={feature.id}>
<Synopsis
id={feature.id}
onClick={createAppNavigationHandler(feature.path)}
description={feature.description}
iconType={feature.icon}
title={feature.title}
url={addBasePath(feature.path)}
wrapInPanel
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</section>
</>
);
ManageData.propTypes = {
addBasePath: PropTypes.func.isRequired,
features: 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,
order: PropTypes.number,
})
),
};

View file

@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SolutionPanel renders the solution panel for the given solution 1`] = `
<EuiFlexItem
className="homSolutions__group homSolutions__group--single homSolutions__item"
grow={1}
key="kibana"
>
<a
className="homSolutionPanel homSolutionPanel--kibana"
href="kibana_landing_page"
onClick={[Function]}
>
<EuiPanel
paddingSize="none"
>
<EuiFlexGroup
gutterSize="none"
>
<EuiFlexItem
className="homSolutionPanel__header"
grow={1}
>
<SolutionTitle
iconType="logoKibana"
subtitle="Visualize & analyze"
title="Kibana"
/>
</EuiFlexItem>
<EuiFlexItem
className="homSolutionPanel__content"
grow={1}
>
<EuiText
key="Analyze data in dashboards"
size="s"
>
<p>
Analyze data in dashboards
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</a>
</EuiFlexItem>
`;

View file

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SolutionTitle renders the title section of the solution panel 1`] = `
<EuiFlexGroup
alignItems="center"
gutterSize="none"
>
<EuiFlexItem
className="eui-textCenter"
>
<EuiToken
className="homSolutionPanel__icon"
fill="light"
iconType="logoKibana"
shape="circle"
size="l"
/>
<EuiTitle
className="eui-textInheritColor"
size="s"
>
<h3>
Kibana
</h3>
</EuiTitle>
<EuiText
size="s"
>
<p
className="homSolutionPanel__subtitle"
>
Visualize & analyze
<EuiIcon
type="sortRight"
/>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,288 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SolutionsSection only renders a spacer if no solutions are available 1`] = `
<Fragment>
<section
aria-labelledby="homSolutions__title"
className="homSolutions"
>
<EuiScreenReaderOnly>
<EuiTitle
size="s"
>
<h2
id="homSolutions__title"
>
<FormattedMessage
defaultMessage="Pick your solution"
id="home.solutionsSection.sectionTitle"
values={Object {}}
/>
</h2>
</EuiTitle>
</EuiScreenReaderOnly>
<EuiFlexGroup
className="homSolutions__content"
justifyContent="spaceAround"
/>
</section>
<EuiHorizontalRule
aria-hidden="true"
margin="xl"
/>
</Fragment>
`;
exports[`SolutionsSection renders a single solution 1`] = `
<Fragment>
<section
aria-labelledby="homSolutions__title"
className="homSolutions"
>
<EuiScreenReaderOnly>
<EuiTitle
size="s"
>
<h2
id="homSolutions__title"
>
<FormattedMessage
defaultMessage="Pick your solution"
id="home.solutionsSection.sectionTitle"
values={Object {}}
/>
</h2>
</EuiTitle>
</EuiScreenReaderOnly>
<EuiFlexGroup
className="homSolutions__content"
justifyContent="spaceAround"
>
<SolutionPanel
addBasePath={[Function]}
solution={
Object {
"descriptions": Array [
"Analyze data in dashboards",
],
"icon": "logoKibana",
"id": "kibana",
"order": 1,
"path": "kibana_landing_page",
"subtitle": "Visualize & analyze",
"title": "Kibana",
}
}
/>
</EuiFlexGroup>
</section>
<EuiHorizontalRule
aria-hidden="true"
margin="xl"
/>
</Fragment>
`;
exports[`SolutionsSection renders multiple solutions in a single column when Kibana apps are not enabled 1`] = `
<Fragment>
<section
aria-labelledby="homSolutions__title"
className="homSolutions"
>
<EuiScreenReaderOnly>
<EuiTitle
size="s"
>
<h2
id="homSolutions__title"
>
<FormattedMessage
defaultMessage="Pick your solution"
id="home.solutionsSection.sectionTitle"
values={Object {}}
/>
</h2>
</EuiTitle>
</EuiScreenReaderOnly>
<EuiFlexGroup
className="homSolutions__content"
justifyContent="spaceAround"
>
<EuiFlexItem
className="homSolutions__group homSolutions__group--multiple"
grow={1}
>
<EuiFlexGroup
direction="column"
>
<SolutionPanel
addBasePath={[Function]}
key="solution-2"
solution={
Object {
"descriptions": Array [
"Example use case",
],
"icon": "empty",
"id": "solution-2",
"order": 2,
"path": "path-to-solution-two",
"subtitle": "Subtitle for solution two",
"title": "Solution two",
}
}
/>
<SolutionPanel
addBasePath={[Function]}
key="solution-3"
solution={
Object {
"descriptions": Array [
"Example use case",
],
"icon": "empty",
"id": "solution-3",
"order": 3,
"path": "path-to-solution-three",
"subtitle": "Subtitle for solution three",
"title": "Solution three",
}
}
/>
<SolutionPanel
addBasePath={[Function]}
key="solution-4"
solution={
Object {
"descriptions": Array [
"Example use case",
],
"icon": "empty",
"id": "solution-4",
"order": 4,
"path": "path-to-solution-four",
"subtitle": "Subtitle for solution four",
"title": "Solution four",
}
}
/>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</section>
<EuiHorizontalRule
aria-hidden="true"
margin="xl"
/>
</Fragment>
`;
exports[`SolutionsSection renders multiple solutions in two columns with Kibana in its own column 1`] = `
<Fragment>
<section
aria-labelledby="homSolutions__title"
className="homSolutions"
>
<EuiScreenReaderOnly>
<EuiTitle
size="s"
>
<h2
id="homSolutions__title"
>
<FormattedMessage
defaultMessage="Pick your solution"
id="home.solutionsSection.sectionTitle"
values={Object {}}
/>
</h2>
</EuiTitle>
</EuiScreenReaderOnly>
<EuiFlexGroup
className="homSolutions__content"
justifyContent="spaceAround"
>
<EuiFlexItem
className="homSolutions__group homSolutions__group--multiple"
grow={1}
>
<EuiFlexGroup
direction="column"
>
<SolutionPanel
addBasePath={[Function]}
key="solution-2"
solution={
Object {
"descriptions": Array [
"Example use case",
],
"icon": "empty",
"id": "solution-2",
"order": 2,
"path": "path-to-solution-two",
"subtitle": "Subtitle for solution two",
"title": "Solution two",
}
}
/>
<SolutionPanel
addBasePath={[Function]}
key="solution-3"
solution={
Object {
"descriptions": Array [
"Example use case",
],
"icon": "empty",
"id": "solution-3",
"order": 3,
"path": "path-to-solution-three",
"subtitle": "Subtitle for solution three",
"title": "Solution three",
}
}
/>
<SolutionPanel
addBasePath={[Function]}
key="solution-4"
solution={
Object {
"descriptions": Array [
"Example use case",
],
"icon": "empty",
"id": "solution-4",
"order": 4,
"path": "path-to-solution-four",
"subtitle": "Subtitle for solution four",
"title": "Solution four",
}
}
/>
</EuiFlexGroup>
</EuiFlexItem>
<SolutionPanel
addBasePath={[Function]}
solution={
Object {
"descriptions": Array [
"Analyze data in dashboards",
],
"icon": "logoKibana",
"id": "kibana",
"order": 1,
"path": "kibana_landing_page",
"subtitle": "Visualize & analyze",
"title": "Kibana",
}
}
/>
</EuiFlexGroup>
</section>
<EuiHorizontalRule
aria-hidden="true"
margin="xl"
/>
</Fragment>
`;

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export * from './solutions_section';

View file

@ -0,0 +1,43 @@
/*
* 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.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { SolutionPanel } from './solution_panel';
const solutionEntry = {
id: 'kibana',
title: 'Kibana',
subtitle: 'Visualize & analyze',
descriptions: ['Analyze data in dashboards'],
icon: 'logoKibana',
path: 'kibana_landing_page',
order: 1,
};
const addBasePathMock = (path: string) => (path ? path : 'path');
describe('SolutionPanel', () => {
test('renders the solution panel for the given solution', () => {
const component = shallow(
<SolutionPanel addBasePath={addBasePathMock} solution={solutionEntry} />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,83 @@
/*
* 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.
*/
import React, { FC } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
import { FeatureCatalogueSolution } from '../../../';
import { createAppNavigationHandler } from '../app_navigation_handler';
import { SolutionTitle } from './solution_title';
const getDescriptionText = (description: string): JSX.Element => (
<EuiText size="s" key={`${description}`}>
<p>{description}</p>
</EuiText>
);
const addSpacersBetweenElementsReducer = (
acc: JSX.Element[],
element: JSX.Element,
index: number,
elements: JSX.Element[]
) => {
acc.push(element);
if (index < elements.length - 1) {
acc.push(<EuiSpacer key={`homeSolutionsPanel__UseCaseSpacer${index}`} size="m" />);
}
return acc;
};
const getDescriptions = (descriptions: string[]) =>
descriptions.map(getDescriptionText).reduce<JSX.Element[]>(addSpacersBetweenElementsReducer, []);
interface Props {
addBasePath: (path: string) => string;
solution: FeatureCatalogueSolution;
}
export const SolutionPanel: FC<Props> = ({ addBasePath, solution }) => (
<EuiFlexItem
key={solution.id}
className={`${
solution.id === 'kibana' ? 'homSolutions__group homSolutions__group--single' : ''
} homSolutions__item`}
grow={1}
>
<a
className={`homSolutionPanel homSolutionPanel--${solution.id}`}
href={addBasePath(solution.path)}
onClick={createAppNavigationHandler(solution.path)}
>
<EuiPanel paddingSize="none">
<EuiFlexGroup gutterSize="none">
<EuiFlexItem grow={1} className={`homSolutionPanel__header`}>
<SolutionTitle
iconType={solution.icon}
title={solution.title}
subtitle={solution.subtitle}
/>
</EuiFlexItem>
<EuiFlexItem grow={1} className="homSolutionPanel__content">
{getDescriptions(solution.descriptions)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</a>
</EuiFlexItem>
);

View file

@ -0,0 +1,45 @@
/*
* 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.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { SolutionTitle } from './solution_title';
const solutionEntry = {
id: 'kibana',
title: 'Kibana',
subtitle: 'Visualize & analyze',
descriptions: ['Analyze data in dashboards'],
icon: 'logoKibana',
path: 'kibana_landing_page',
order: 1,
};
describe('SolutionTitle', () => {
test('renders the title section of the solution panel', () => {
const component = shallow(
<SolutionTitle
title={solutionEntry.title}
subtitle={solutionEntry.subtitle}
iconType={solutionEntry.icon}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,59 @@
/*
* 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.
*/
import React, { FC } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiToken,
EuiTitle,
EuiText,
EuiIcon,
IconType,
} from '@elastic/eui';
interface Props {
title: string;
subtitle: string;
iconType: IconType;
}
export const SolutionTitle: FC<Props> = ({ title, subtitle, iconType }) => (
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem className="eui-textCenter">
<EuiToken
iconType={iconType}
shape="circle"
fill="light"
size="l"
className="homSolutionPanel__icon"
/>
<EuiTitle className="eui-textInheritColor" size="s">
<h3>{title}</h3>
</EuiTitle>
<EuiText size="s">
<p className="homSolutionPanel__subtitle">
{subtitle} <EuiIcon type="sortRight" />
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,94 @@
/*
* 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.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { SolutionsSection } from './solutions_section';
const solutionEntry1 = {
id: 'kibana',
title: 'Kibana',
subtitle: 'Visualize & analyze',
descriptions: ['Analyze data in dashboards'],
icon: 'logoKibana',
path: 'kibana_landing_page',
order: 1,
};
const solutionEntry2 = {
id: 'solution-2',
title: 'Solution two',
subtitle: 'Subtitle for solution two',
descriptions: ['Example use case'],
icon: 'empty',
path: 'path-to-solution-two',
order: 2,
};
const solutionEntry3 = {
id: 'solution-3',
title: 'Solution three',
subtitle: 'Subtitle for solution three',
descriptions: ['Example use case'],
icon: 'empty',
path: 'path-to-solution-three',
order: 3,
};
const solutionEntry4 = {
id: 'solution-4',
title: 'Solution four',
subtitle: 'Subtitle for solution four',
descriptions: ['Example use case'],
icon: 'empty',
path: 'path-to-solution-four',
order: 4,
};
const addBasePathMock = (path: string) => (path ? path : 'path');
describe('SolutionsSection', () => {
test('only renders a spacer if no solutions are available', () => {
const component = shallow(<SolutionsSection addBasePath={addBasePathMock} solutions={[]} />);
expect(component).toMatchSnapshot();
});
test('renders a single solution', () => {
const component = shallow(
<SolutionsSection addBasePath={addBasePathMock} solutions={[solutionEntry1]} />
);
expect(component).toMatchSnapshot();
});
test('renders multiple solutions in two columns with Kibana in its own column', () => {
const component = shallow(
<SolutionsSection
addBasePath={addBasePathMock}
solutions={[solutionEntry1, solutionEntry2, solutionEntry3, solutionEntry4]}
/>
);
expect(component).toMatchSnapshot();
});
test('renders multiple solutions in a single column when Kibana apps are not enabled', () => {
const component = shallow(
<SolutionsSection
addBasePath={addBasePathMock}
solutions={[solutionEntry2, solutionEntry3, solutionEntry4]}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,93 @@
/*
* 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.
*/
import React, { FC } from 'react';
import PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiScreenReaderOnly,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { SolutionPanel } from './solution_panel';
import { FeatureCatalogueSolution } from '../../../';
const sortByOrder = (
{ order: orderA = 0 }: FeatureCatalogueSolution,
{ order: orderB = 0 }: FeatureCatalogueSolution
) => orderA - orderB;
interface Props {
addBasePath: (path: string) => string;
solutions: FeatureCatalogueSolution[];
}
export const SolutionsSection: FC<Props> = ({ addBasePath, solutions }) => {
// Separate Kibana from other solutions
const kibana = solutions.find(({ id }) => id === 'kibana');
solutions = solutions.sort(sortByOrder).filter(({ id }) => id !== 'kibana');
return (
<>
<section aria-labelledby="homSolutions__title" className="homSolutions">
<EuiScreenReaderOnly>
<EuiTitle size="s">
<h2 id="homSolutions__title">
<FormattedMessage
id="home.solutionsSection.sectionTitle"
defaultMessage="Pick your solution"
/>
</h2>
</EuiTitle>
</EuiScreenReaderOnly>
<EuiFlexGroup className="homSolutions__content" justifyContent="spaceAround">
{solutions.length ? (
<EuiFlexItem grow={1} className="homSolutions__group homSolutions__group--multiple">
<EuiFlexGroup direction="column">
{solutions.map((solution) => (
<SolutionPanel key={solution.id} solution={solution} addBasePath={addBasePath} />
))}
</EuiFlexGroup>
</EuiFlexItem>
) : null}
{kibana ? <SolutionPanel solution={kibana} addBasePath={addBasePath} /> : null}
</EuiFlexGroup>
</section>
<EuiHorizontalRule margin="xl" aria-hidden="true" />
</>
);
};
SolutionsSection.propTypes = {
solutions: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
descriptions: PropTypes.arrayOf(PropTypes.string).isRequired,
icon: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
order: PropTypes.number,
})
),
};

View file

@ -24,6 +24,7 @@ import classNames from 'classnames';
import { EuiCard, EuiIcon } from '@elastic/eui';
export function Synopsis({
id,
description,
iconUrl,
iconType,
@ -54,8 +55,9 @@ export function Synopsis({
description={description}
onClick={onClick}
href={url}
data-test-subj={`homeSynopsisLink${title.toLowerCase()}`}
data-test-subj={`homeSynopsisLink${id.toLowerCase()}`}
betaBadgeLabel={isBeta ? 'Beta' : null}
titleElement="h3"
/>
);
}

View file

@ -25,6 +25,7 @@ import { Synopsis } from './synopsis';
test('render', () => {
const component = shallow(
<Synopsis
id={'tutorial'}
description="this is a great tutorial about..."
title="Great tutorial"
url="link_to_item"
@ -37,6 +38,7 @@ describe('props', () => {
test('iconType', () => {
const component = shallow(
<Synopsis
id={'tutorial'}
description="this is a great tutorial about..."
title="Great tutorial"
url="link_to_item"
@ -49,6 +51,7 @@ describe('props', () => {
test('iconUrl', () => {
const component = shallow(
<Synopsis
id={'tutorial'}
description="this is a great tutorial about..."
title="Great tutorial"
url="link_to_item"
@ -61,6 +64,7 @@ describe('props', () => {
test('isBeta', () => {
const component = shallow(
<Synopsis
id={'tutorial'}
description="this is a great tutorial about..."
title="Great tutorial"
url="link_to_item"

View file

@ -136,6 +136,7 @@ class TutorialDirectoryUi extends React.Component {
}
return {
id: tutorialConfig.id,
category: tutorialConfig.category,
icon: icon,
name: tutorialConfig.name,
@ -149,6 +150,7 @@ class TutorialDirectoryUi extends React.Component {
// Add card for sample data that only gets show in "all" tab
tutorialCards.push({
id: 'sample_data',
name: this.props.intl.formatMessage({
id: 'home.tutorial.card.sampleDataTitle',
defaultMessage: 'Sample Data',
@ -214,6 +216,7 @@ class TutorialDirectoryUi extends React.Component {
return (
<EuiFlexItem key={tutorial.name}>
<Synopsis
id={tutorial.id}
iconType={tutorial.icon}
description={tutorial.description}
title={tutorial.name}

View file

@ -27,6 +27,7 @@ export {
} from './plugin';
export {
FeatureCatalogueEntry,
FeatureCatalogueSolution,
FeatureCatalogueCategory,
Environment,
TutorialVariables,

View file

@ -33,6 +33,42 @@ describe('HomePublicPlugin', () => {
});
describe('setup', () => {
test('registers tutorial directory to feature catalogue', async () => {
const setup = await new HomePublicPlugin(mockInitializerContext).setup(
coreMock.createSetup() as any,
{
kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(),
}
);
expect(setup).toHaveProperty('featureCatalogue');
expect(setup.featureCatalogue.register).toHaveBeenCalledTimes(1);
expect(setup.featureCatalogue.register).toHaveBeenCalledWith(
expect.objectContaining({
category: 'data',
icon: 'indexOpen',
id: 'home_tutorial_directory',
showOnHomePage: true,
})
);
});
test('registers kibana solution to feature catalogue', async () => {
const setup = await new HomePublicPlugin(mockInitializerContext).setup(
coreMock.createSetup() as any,
{
kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(),
}
);
expect(setup).toHaveProperty('featureCatalogue');
expect(setup.featureCatalogue.registerSolution).toHaveBeenCalledTimes(1);
expect(setup.featureCatalogue.registerSolution).toHaveBeenCalledWith(
expect.objectContaining({
icon: 'logoKibana',
id: 'kibana',
})
);
});
test('wires up and returns registry', async () => {
const setup = await new HomePublicPlugin(mockInitializerContext).setup(
coreMock.createSetup() as any,

View file

@ -30,6 +30,7 @@ import { first } from 'rxjs/operators';
import {
EnvironmentService,
EnvironmentServiceSetup,
FeatureCatalogueCategory,
FeatureCatalogueRegistry,
FeatureCatalogueRegistrySetup,
TutorialService,
@ -42,6 +43,7 @@ import { TelemetryPluginStart } from '../../telemetry/public';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { KibanaLegacySetup, KibanaLegacyStart } from '../../kibana_legacy/public';
import { AppNavLinkStatus } from '../../../core/public';
import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants';
export interface HomePluginStartDependencies {
data: DataPublicPluginStart;
@ -68,7 +70,7 @@ export class HomePublicPlugin
{ kibanaLegacy, usageCollection }: HomePluginSetupDependencies
): HomePublicPluginSetup {
core.application.register({
id: 'home',
id: PLUGIN_ID,
title: 'Home',
navLinkStatus: AppNavLinkStatus.hidden,
mount: async (params: AppMountParameters) => {
@ -109,8 +111,58 @@ export class HomePublicPlugin
});
kibanaLegacy.forwardApp('home', 'home');
const featureCatalogue = { ...this.featuresCatalogueRegistry.setup() };
featureCatalogue.register({
id: 'home_tutorial_directory',
title: i18n.translate('home.tutorialDirectory.featureCatalogueTitle', {
defaultMessage: 'Add data',
}),
description: i18n.translate('home.tutorialDirectory.featureCatalogueDescription', {
defaultMessage: 'Ingest data from popular apps and services.',
}),
icon: 'indexOpen',
showOnHomePage: true,
path: `${HOME_APP_BASE_PATH}#/tutorial_directory`,
category: 'data' as FeatureCatalogueCategory.DATA,
order: 500,
});
featureCatalogue.registerSolution({
id: 'kibana',
title: i18n.translate('home.kibana.featureCatalogue.title', {
defaultMessage: 'Kibana',
}),
subtitle: i18n.translate('home.kibana.featureCatalogue.subtitle', {
defaultMessage: 'Visualize & analyze',
}),
descriptions: [
i18n.translate('home.kibana.featureCatalogueDescription1', {
defaultMessage: 'Analyze data in dashboards.',
}),
i18n.translate('home.kibana.featureCatalogueDescription2', {
defaultMessage: 'Search and find insights.',
}),
i18n.translate('home.kibana.featureCatalogueDescription3', {
defaultMessage: 'Design pixel-perfect reports.',
}),
i18n.translate('home.kibana.featureCatalogueDescription4', {
defaultMessage: 'Plot geographic data.',
}),
i18n.translate('home.kibana.featureCatalogueDescription5', {
defaultMessage: 'Model, predict, and detect.',
}),
i18n.translate('home.kibana.featureCatalogueDescription6', {
defaultMessage: 'Reveal patterns and relationships.',
}),
],
icon: 'logoKibana',
path: '/app/dashboards',
order: 400,
});
return {
featureCatalogue: { ...this.featuresCatalogueRegistry.setup() },
featureCatalogue,
environment: { ...this.environmentService.setup() },
tutorials: { ...this.tutorialService.setup() },
};
@ -124,7 +176,7 @@ export class HomePublicPlugin
// If the home app is the initial location when loading Kibana...
if (
window.location.pathname === http.basePath.prepend(`/app/home`) &&
window.location.pathname === http.basePath.prepend(HOME_APP_BASE_PATH) &&
window.location.hash === ''
) {
// ...wait for the app to mount initially and then...
@ -157,5 +209,6 @@ export interface HomePublicPluginSetup {
* be replaced by display specific extension points.
* @deprecated
*/
environment: EnvironmentSetup;
}

View file

@ -25,6 +25,7 @@ import {
const createSetupMock = (): jest.Mocked<FeatureCatalogueRegistrySetup> => {
const setup = {
register: jest.fn(),
registerSolution: jest.fn(),
};
return setup;
};
@ -34,6 +35,7 @@ const createMock = (): jest.Mocked<PublicMethodsOf<FeatureCatalogueRegistry>> =>
setup: jest.fn(),
start: jest.fn(),
get: jest.fn(() => []),
getSolutions: jest.fn(() => []),
};
service.setup.mockImplementation(createSetupMock);
return service;

View file

@ -21,6 +21,7 @@ import {
FeatureCatalogueRegistry,
FeatureCatalogueCategory,
FeatureCatalogueEntry,
FeatureCatalogueSolution,
} from './feature_catalogue_registry';
const DASHBOARD_FEATURE: FeatureCatalogueEntry = {
@ -33,15 +34,32 @@ const DASHBOARD_FEATURE: FeatureCatalogueEntry = {
category: FeatureCatalogueCategory.DATA,
};
const KIBANA_SOLUTION: FeatureCatalogueSolution = {
id: 'kibana',
title: 'Kibana',
subtitle: 'Visualize & analyze',
descriptions: ['Analyze data in dashboards.', 'Search and find insights.'],
icon: 'kibanaApp',
path: `/app/home`,
};
describe('FeatureCatalogueRegistry', () => {
describe('setup', () => {
test('throws when registering duplicate id', () => {
test('throws when registering a feature with a duplicate id', () => {
const setup = new FeatureCatalogueRegistry().setup();
setup.register(DASHBOARD_FEATURE);
expect(() => setup.register(DASHBOARD_FEATURE)).toThrowErrorMatchingInlineSnapshot(
`"Feature with id [dashboard] has already been registered. Use a unique id."`
);
});
test('throws when registering a solution with a duplicate id', () => {
const setup = new FeatureCatalogueRegistry().setup();
setup.registerSolution(KIBANA_SOLUTION);
expect(() => setup.registerSolution(KIBANA_SOLUTION)).toThrowErrorMatchingInlineSnapshot(
`"Solution with id [kibana] has already been registered. Use a unique id."`
);
});
});
describe('start', () => {

View file

@ -43,11 +43,32 @@ export interface FeatureCatalogueEntry {
readonly path: string;
/** Whether or not this link should be shown on the front page of Kibana. */
readonly showOnHomePage: boolean;
/** An ordinal used to sort features relative to one another for display on the home page */
readonly order?: number;
}
/** @public */
export interface FeatureCatalogueSolution {
/** Unique string identifier for this solution. */
readonly id: string;
/** Title of solution displayed to the user. */
readonly title: string;
/** The tagline of the solution displayed to the user. */
readonly subtitle: string;
/** A list of use cases for this solution displayed to the user. */
readonly descriptions: string[];
/** EUI `IconType` for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or ReactElement. */
readonly icon: IconType;
/** URL path to link to this future. Should not include the basePath. */
readonly path: string;
/** An ordinal used to sort solutions relative to one another for display on the home page */
readonly order?: number;
}
export class FeatureCatalogueRegistry {
private capabilities: Capabilities | null = null;
private readonly features = new Map<string, FeatureCatalogueEntry>();
private readonly solutions = new Map<string, FeatureCatalogueSolution>();
public setup() {
return {
@ -60,6 +81,15 @@ export class FeatureCatalogueRegistry {
this.features.set(feature.id, feature);
},
registerSolution: (solution: FeatureCatalogueSolution) => {
if (this.solutions.has(solution.id)) {
throw new Error(
`Solution with id [${solution.id}] has already been registered. Use a unique id.`
);
}
this.solutions.set(solution.id, solution);
},
};
}
@ -76,6 +106,16 @@ export class FeatureCatalogueRegistry {
.filter((entry) => capabilities.catalogue[entry.id] !== false)
.sort(compareByKey('title'));
}
public getSolutions(): readonly FeatureCatalogueSolution[] {
if (this.capabilities === null) {
throw new Error('Catalogue entries are only available after start phase');
}
const capabilities = this.capabilities;
return [...this.solutions.values()]
.filter((solution) => capabilities.catalogue[solution.id] !== false)
.sort(compareByKey('title'));
}
}
export type FeatureCatalogueRegistrySetup = ReturnType<FeatureCatalogueRegistry['setup']>;

View file

@ -20,6 +20,7 @@
export {
FeatureCatalogueCategory,
FeatureCatalogueEntry,
FeatureCatalogueSolution,
FeatureCatalogueRegistry,
FeatureCatalogueRegistrySetup,
} from './feature_catalogue_registry';

View file

@ -3,6 +3,7 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["kibanaLegacy", "home"],
"requiredBundles": ["kibanaReact", "kibanaUtils"]
"requiredPlugins": ["kibanaLegacy"],
"optionalPlugins": ["home"],
"requiredBundles": ["kibanaReact", "kibanaUtils", "home"]
}

View file

@ -35,7 +35,7 @@ import {
} from './management_sections_service';
interface ManagementSetupDependencies {
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
}
export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart> {
@ -46,19 +46,21 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart
public setup(core: CoreSetup, { home }: ManagementSetupDependencies) {
const kibanaVersion = this.initializerContext.env.packageInfo.version;
home.featureCatalogue.register({
id: 'stack-management',
title: i18n.translate('management.stackManagement.managementLabel', {
defaultMessage: 'Stack Management',
}),
description: i18n.translate('management.stackManagement.managementDescription', {
defaultMessage: 'Your center console for managing the Elastic Stack.',
}),
icon: 'managementApp',
path: '/app/management',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
if (home) {
home.featureCatalogue.register({
id: 'stack-management',
title: i18n.translate('management.stackManagement.managementLabel', {
defaultMessage: 'Stack Management',
}),
description: i18n.translate('management.stackManagement.managementDescription', {
defaultMessage: 'Your center console for managing the Elastic Stack.',
}),
icon: 'managementApp',
path: '/app/management',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
}
core.application.register({
id: 'management',

View file

@ -3,8 +3,8 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["home", "management", "data"],
"optionalPlugins": ["dashboard", "visualizations", "discover"],
"requiredPlugins": ["management", "data"],
"optionalPlugins": ["dashboard", "visualizations", "discover", "home"],
"extraPublicDirs": ["public/lib"],
"requiredBundles": ["kibanaReact"]
"requiredBundles": ["kibanaReact", "home"]
}

View file

@ -45,7 +45,7 @@ export interface SavedObjectsManagementPluginStart {
export interface SetupDependencies {
management: ManagementSetup;
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
}
export interface StartDependencies {
@ -72,20 +72,22 @@ export class SavedObjectsManagementPlugin
): SavedObjectsManagementPluginSetup {
const actionSetup = this.actionService.setup();
home.featureCatalogue.register({
id: 'saved_objects',
title: i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', {
defaultMessage: 'Saved Objects',
}),
description: i18n.translate('savedObjectsManagement.objects.savedObjectsDescription', {
defaultMessage:
'Import, export, and manage your saved searches, visualizations, and dashboards.',
}),
icon: 'savedObjectsApp',
path: '/app/management/kibana/objects',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN,
});
if (home) {
home.featureCatalogue.register({
id: 'saved_objects',
title: i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', {
defaultMessage: 'Saved Objects',
}),
description: i18n.translate('savedObjectsManagement.objects.savedObjectsDescription', {
defaultMessage:
'Import, export, and manage your saved searches, visualizations, and dashboards.',
}),
icon: 'savedObjectsApp',
path: '/app/management/kibana/objects',
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
}
const kibanaSection = management.sections.section.kibana;
kibanaSection.registerApp({

View file

@ -221,7 +221,7 @@ export class VisualizePlugin
}),
icon: 'visualizeApp',
path: `/app/visualize#${VisualizeConstants.LANDING_PAGE_PATH}`,
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
});
}

View file

@ -98,19 +98,19 @@ export function HomePageProvider({ getService, getPageObjects }: FtrProviderCont
}
async clickOnConsole() {
await testSubjects.click('homeSynopsisLinkconsole');
await this.clickSynopsis('console');
}
async clickOnLogo() {
await testSubjects.click('logo');
}
async ClickOnLogsData() {
await testSubjects.click('logsData');
async clickOnAddData() {
await this.clickSynopsis('home_tutorial_directory');
}
// clicks on Active MQ logs
async clickOnLogsTutorial() {
await testSubjects.click('homeSynopsisLinkactivemq logs');
await this.clickSynopsis('activemqlogs');
}
// clicks on cloud tutorial link

View file

@ -6,7 +6,6 @@
"features",
"apmOss",
"data",
"home",
"licensing",
"triggers_actions_ui"
],
@ -18,7 +17,8 @@
"alerts",
"observability",
"security",
"ml"
"ml",
"home"
],
"server": true,
"ui": true,
@ -32,6 +32,7 @@
"requiredBundles": [
"kibanaReact",
"kibanaUtils",
"observability"
"observability",
"home"
]
}

View file

@ -17,6 +17,6 @@ export const featureCatalogueEntry = {
}),
icon: 'apmApp',
path: '/app/apm',
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
};

View file

@ -45,7 +45,7 @@ export interface ApmPluginSetupDeps {
alerts?: AlertingPluginPublicSetup;
data: DataPublicPluginSetup;
features: FeaturesPluginSetup;
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
licensing: LicensingPluginSetup;
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
observability?: ObservabilityPluginSetup;
@ -69,8 +69,10 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
const config = this.initializerContext.config.get();
const pluginSetupDeps = plugins;
pluginSetupDeps.home.environment.update({ apmUi: true });
pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry);
if (pluginSetupDeps.home) {
pluginSetupDeps.home.environment.update({ apmUi: true });
pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry);
}
if (plugins.observability) {
const getApmDataHelper = async () => {

View file

@ -5,7 +5,7 @@
"configPath": ["xpack", "canvas"],
"server": true,
"ui": true,
"requiredPlugins": ["bfetch", "data", "embeddable", "expressions", "features", "home", "inspector", "uiActions"],
"optionalPlugins": ["usageCollection"],
"requiredBundles": ["kibanaReact", "maps", "lens", "visualizations", "kibanaUtils", "kibanaLegacy", "discover", "savedObjects", "reporting"]
"requiredPlugins": ["bfetch", "data", "embeddable", "expressions", "features", "inspector", "uiActions"],
"optionalPlugins": ["usageCollection", "home"],
"requiredBundles": ["kibanaReact", "maps", "lens", "visualizations", "kibanaUtils", "kibanaLegacy", "discover", "savedObjects", "reporting", "home"]
}

View file

@ -15,6 +15,6 @@ export const featureCatalogueEntry = {
}),
icon: 'canvasApp',
path: '/app/canvas',
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
};

View file

@ -40,7 +40,7 @@ export { CoreStart, CoreSetup };
export interface CanvasSetupDeps {
data: DataPublicPluginSetup;
expressions: ExpressionsSetup;
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
usageCollection?: UsageCollectionSetup;
bfetch: BfetchPublicSetup;
}
@ -116,7 +116,9 @@ export class CanvasPlugin
},
});
plugins.home.featureCatalogue.register(featureCatalogueEntry);
if (plugins.home) {
plugins.home.featureCatalogue.register(featureCatalogueEntry);
}
canvasApi.addArgumentUIs(argTypeSpecs);
canvasApi.addTransitions(transitions);

View file

@ -2,9 +2,10 @@
"id": "enterpriseSearch",
"version": "kibana",
"kibanaVersion": "kibana",
"requiredPlugins": ["home", "features", "licensing"],
"requiredPlugins": ["features", "licensing"],
"configPath": ["enterpriseSearch"],
"optionalPlugins": ["usageCollection", "security"],
"optionalPlugins": ["usageCollection", "security", "home"],
"server": true,
"ui": true
"ui": true,
"requiredBundles": ["home"]
}

View file

@ -12,7 +12,7 @@ import {
AppMountParameters,
HttpSetup,
} from 'src/core/public';
import { i18n } from '@kbn/i18n';
import {
FeatureCatalogueCategory,
HomePublicPluginSetup,
@ -21,7 +21,11 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { LicensingPluginSetup } from '../../licensing/public';
import { IInitialAppData } from '../common/types';
import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../common/constants';
import {
ENTERPRISE_SEARCH_PLUGIN,
APP_SEARCH_PLUGIN,
WORKPLACE_SEARCH_PLUGIN,
} from '../common/constants';
import { ExternalUrl, IExternalUrl } from './applications/shared/enterprise_search_url';
import AppSearchLogo from './applications/app_search/assets/logo.svg';
import WorkplaceSearchLogo from './applications/workplace_search/assets/logo.svg';
@ -35,7 +39,7 @@ export interface ClientData extends IInitialAppData {
}
export interface PluginsSetup {
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
licensing: LicensingPluginSetup;
}
@ -88,25 +92,48 @@ export class EnterpriseSearchPlugin implements Plugin {
},
});
plugins.home.featureCatalogue.register({
id: APP_SEARCH_PLUGIN.ID,
title: APP_SEARCH_PLUGIN.NAME,
icon: AppSearchLogo,
description: APP_SEARCH_PLUGIN.DESCRIPTION,
path: APP_SEARCH_PLUGIN.URL,
category: FeatureCatalogueCategory.DATA,
showOnHomePage: true,
});
if (plugins.home) {
plugins.home.featureCatalogue.registerSolution({
id: ENTERPRISE_SEARCH_PLUGIN.ID,
title: ENTERPRISE_SEARCH_PLUGIN.NAME,
subtitle: i18n.translate('xpack.enterpriseSearch.featureCatalogue.subtitle', {
defaultMessage: 'Search everything',
}),
icon: 'logoEnterpriseSearch',
descriptions: [
i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription1', {
defaultMessage: 'Build a powerful search experience.',
}),
i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription2', {
defaultMessage: 'Connect your users to relevant data.',
}),
i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription3', {
defaultMessage: 'Unify your team content.',
}),
],
path: APP_SEARCH_PLUGIN.URL, // TODO: Change this to enterprise search overview page once available
});
plugins.home.featureCatalogue.register({
id: WORKPLACE_SEARCH_PLUGIN.ID,
title: WORKPLACE_SEARCH_PLUGIN.NAME,
icon: WorkplaceSearchLogo,
description: WORKPLACE_SEARCH_PLUGIN.DESCRIPTION,
path: WORKPLACE_SEARCH_PLUGIN.URL,
category: FeatureCatalogueCategory.DATA,
showOnHomePage: true,
});
plugins.home.featureCatalogue.register({
id: APP_SEARCH_PLUGIN.ID,
title: APP_SEARCH_PLUGIN.NAME,
icon: AppSearchLogo,
description: APP_SEARCH_PLUGIN.DESCRIPTION,
path: APP_SEARCH_PLUGIN.URL,
category: FeatureCatalogueCategory.DATA,
showOnHomePage: false,
});
plugins.home.featureCatalogue.register({
id: WORKPLACE_SEARCH_PLUGIN.ID,
title: WORKPLACE_SEARCH_PLUGIN.NAME,
icon: WorkplaceSearchLogo,
description: WORKPLACE_SEARCH_PLUGIN.DESCRIPTION,
path: WORKPLACE_SEARCH_PLUGIN.URL,
category: FeatureCatalogueCategory.DATA,
showOnHomePage: false,
});
}
}
public start(core: CoreStart) {}

View file

@ -75,7 +75,7 @@ export class EnterpriseSearchPlugin implements Plugin {
icon: 'logoEnterpriseSearch',
navLinkId: APP_SEARCH_PLUGIN.ID, // TODO - remove this once functional tests no longer rely on navLinkId
app: ['kibana', APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID],
catalogue: [APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID],
catalogue: [ENTERPRISE_SEARCH_PLUGIN.ID, APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID],
privileges: null,
});
@ -93,6 +93,7 @@ export class EnterpriseSearchPlugin implements Plugin {
workplaceSearch: hasWorkplaceSearchAccess,
},
catalogue: {
enterpriseSearch: hasAppSearchAccess || hasWorkplaceSearchAccess,
appSearch: hasAppSearchAccess,
workplaceSearch: hasWorkplaceSearchAccess,
},

View file

@ -61,7 +61,7 @@ export class GraphPlugin
}),
icon: 'graphApp',
path: '/app/graph',
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
});
}
@ -113,7 +113,8 @@ export class GraphPlugin
throw new Error('Start called before setup');
}
this.licensing.license$.subscribe((license) => {
toggleNavLink(checkLicense(license), core.chrome.navLinks);
const licenseInformation = checkLicense(license);
toggleNavLink(licenseInformation, core.chrome.navLinks);
});
}

View file

@ -4,18 +4,19 @@
"server": true,
"ui": true,
"requiredPlugins": [
"home",
"licensing",
"management"
],
"optionalPlugins": [
"usageCollection",
"indexManagement"
"indexManagement",
"home"
],
"configPath": ["xpack", "ilm"],
"requiredBundles": [
"indexManagement",
"kibanaReact",
"esUiShared"
"esUiShared",
"home"
]
}

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, PluginInitializerContext } from 'src/core/public';
import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
import { PLUGIN } from '../common/constants';
import { init as initHttp } from './application/services/http';
import { init as initDocumentation } from './application/services/documentation';
@ -30,7 +31,7 @@ export class IndexLifecycleManagementPlugin {
getStartServices,
} = coreSetup;
const { usageCollection, management, indexManagement } = plugins;
const { usageCollection, management, indexManagement, home } = plugins;
// Initialize services even if the app isn't mounted, because they're used by index management extensions.
initHttp(http);
@ -74,6 +75,24 @@ export class IndexLifecycleManagementPlugin {
},
});
if (home) {
home.featureCatalogue.register({
id: PLUGIN.ID,
title: i18n.translate('xpack.indexLifecycleMgmt.featureCatalogueTitle', {
defaultMessage: 'Manage index lifecycles',
}),
description: i18n.translate('xpack.indexLifecycleMgmt.featureCatalogueDescription', {
defaultMessage:
'Define lifecycle policies to automatically perform operations as an index ages.',
}),
icon: 'indexSettings',
path: '/app/management/data/index_lifecycle_management',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN,
order: 640,
});
}
if (indexManagement) {
addAllExtensions(indexManagement.extensionsService);
}

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { ManagementSetup } from '../../../../src/plugins/management/public';
import { IndexManagementPluginSetup } from '../../index_management/public';
@ -12,6 +13,7 @@ export interface PluginsDependencies {
usageCollection?: UsageCollectionSetup;
management: ManagementSetup;
indexManagement?: IndexManagementPluginSetup;
home?: HomePublicPluginSetup;
}
export interface ClientConfigType {

View file

@ -6,14 +6,14 @@
"features",
"usageCollection",
"spaces",
"home",
"data",
"dataEnhanced",
"visTypeTimeseries",
"alerts",
"triggers_actions_ui"
],
"optionalPlugins": ["ml", "observability"],
"optionalPlugins": ["ml", "observability", "home"],
"server": true,
"ui": true,
"configPath": ["xpack", "infra"],
@ -22,6 +22,7 @@
"licenseManagement",
"kibanaUtils",
"kibanaReact",
"apm"
"apm",
"home"
]
}

View file

@ -25,7 +25,9 @@ export class Plugin implements InfraClientPluginClass {
constructor(_context: PluginInitializerContext) {}
setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) {
registerFeatures(pluginsSetup.home);
if (pluginsSetup.home) {
registerFeatures(pluginsSetup.home);
}
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createInventoryMetricAlertType());
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType());

View file

@ -22,7 +22,7 @@ export const registerFeatures = (homePlugin: HomePublicPluginSetup) => {
}),
icon: 'metricsApp',
path: `/app/metrics`,
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
});
@ -37,7 +37,7 @@ export const registerFeatures = (homePlugin: HomePublicPluginSetup) => {
}),
icon: 'logsApp',
path: `/app/logs`,
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
});
};

View file

@ -25,7 +25,7 @@ export type InfraClientStartExports = void;
export interface InfraClientSetupDeps {
dataEnhanced: DataEnhancedSetup;
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
observability: ObservabilityPluginSetup;
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
usageCollection: UsageCollectionSetup;

View file

@ -18,7 +18,7 @@ export const METRICS_FEATURE = {
icon: 'metricsApp',
navLinkId: 'metrics',
app: ['infra', 'metrics', 'kibana'],
catalogue: ['infraops'],
catalogue: ['infraops', 'metrics'],
management: {
insightsAndAlerting: ['triggersActions'],
},
@ -26,7 +26,7 @@ export const METRICS_FEATURE = {
privileges: {
all: {
app: ['infra', 'metrics', 'kibana'],
catalogue: ['infraops'],
catalogue: ['infraops', 'metrics'],
api: ['infra'],
savedObject: {
all: ['infrastructure-ui-source'],
@ -42,7 +42,7 @@ export const METRICS_FEATURE = {
},
read: {
app: ['infra', 'metrics', 'kibana'],
catalogue: ['infraops'],
catalogue: ['infraops', 'metrics'],
api: ['infra'],
savedObject: {
all: [],
@ -68,12 +68,12 @@ export const LOGS_FEATURE = {
icon: 'logsApp',
navLinkId: 'logs',
app: ['infra', 'logs', 'kibana'],
catalogue: ['infralogging'],
catalogue: ['infralogging', 'logs'],
alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID],
privileges: {
all: {
app: ['infra', 'logs', 'kibana'],
catalogue: ['infralogging'],
catalogue: ['infralogging', 'logs'],
api: ['infra'],
savedObject: {
all: ['infrastructure-ui-source'],
@ -86,7 +86,7 @@ export const LOGS_FEATURE = {
},
read: {
app: ['infra', 'logs', 'kibana'],
catalogue: ['infralogging'],
catalogue: ['infralogging', 'logs'],
api: ['infra'],
alerting: {
all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID],

View file

@ -7,5 +7,5 @@
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects"],
"optionalPlugins": ["security", "features", "cloud", "usageCollection", "home"],
"extraPublicDirs": ["common"],
"requiredBundles": ["kibanaReact", "esUiShared"]
"requiredBundles": ["kibanaReact", "esUiShared", "home"]
}

View file

@ -13,9 +13,13 @@ import {
import { i18n } from '@kbn/i18n';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import {
HomePublicPluginSetup,
FeatureCatalogueCategory,
} from '../../../../src/plugins/home/public';
import { LicensingPluginSetup } from '../../licensing/public';
import { PLUGIN_ID, CheckPermissionsResponse, PostIngestSetupResponse } from '../common';
import { BASE_PATH } from './applications/ingest_manager/constants';
import { IngestManagerConfigType } from '../common/types';
import { setupRouteService, appRoutesService } from '../common';
@ -95,6 +99,21 @@ export class IngestManagerPlugin
deps.home.tutorials.registerDirectoryNotice(PLUGIN_ID, TutorialDirectoryNotice);
deps.home.tutorials.registerDirectoryHeaderLink(PLUGIN_ID, TutorialDirectoryHeaderLink);
deps.home.tutorials.registerModuleNotice(PLUGIN_ID, TutorialModuleNotice);
deps.home.featureCatalogue.register({
id: 'ingestManager',
title: i18n.translate('xpack.ingestManager.featureCatalogueTitle', {
defaultMessage: 'Add Elastic Agent',
}),
description: i18n.translate('xpack.ingestManager.featureCatalogueDescription', {
defaultMessage: 'Add and manage your fleet of Elastic Agents and integrations.',
}),
icon: 'indexManagementApp',
showOnHomePage: true,
path: BASE_PATH,
category: FeatureCatalogueCategory.DATA,
order: 510,
});
}
return {};

View file

@ -179,10 +179,12 @@ export class IngestManagerPlugin
icon: 'savedObjectsApp',
navLinkId: PLUGIN_ID,
app: [PLUGIN_ID, 'kibana'],
catalogue: ['ingestManager'],
privileges: {
all: {
api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`],
app: [PLUGIN_ID, 'kibana'],
catalogue: ['ingestManager'],
savedObject: {
all: allSavedObjectTypes,
read: [],
@ -192,6 +194,7 @@ export class IngestManagerPlugin
read: {
api: [`${PLUGIN_ID}-read`],
app: [PLUGIN_ID, 'kibana'],
catalogue: ['ingestManager'], // TODO: check if this is actually available to read user
savedObject: {
all: [],
read: allSavedObjectTypes,

View file

@ -70,7 +70,7 @@ export class LogstashPlugin implements Plugin<void, void, SetupDeps> {
}),
icon: 'pipelineApp',
path: '/app/management/ingest/pipelines',
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
});

View file

@ -7,7 +7,6 @@
"licensing",
"features",
"inspector",
"home",
"data",
"fileUpload",
"uiActions",
@ -18,12 +17,14 @@
"usageCollection",
"share"
],
"optionalPlugins": ["home"],
"ui": true,
"server": true,
"extraPublicDirs": ["common/constants"],
"requiredBundles": [
"kibanaReact",
"kibanaUtils",
"savedObjects"
"savedObjects",
"home"
]
}

View file

@ -12,10 +12,10 @@ export const featureCatalogueEntry = {
id: APP_ID,
title: getAppTitle(),
description: i18n.translate('xpack.maps.feature.appDescription', {
defaultMessage: 'Explore geospatial data from Elasticsearch and the Elastic Maps Service',
defaultMessage: 'Explore geospatial data from Elasticsearch and the Elastic Maps Service.',
}),
icon: APP_ICON,
path: '/app/maps',
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
};

View file

@ -51,7 +51,7 @@ import { StartContract as FileUploadStartContract } from '../../file_upload/publ
export interface MapsPluginSetupDependencies {
inspector: InspectorSetupContract;
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
visualizations: VisualizationsSetup;
embeddable: EmbeddableSetup;
mapsLegacy: { config: MapsLegacyConfigType };
@ -108,7 +108,9 @@ export class MapsPlugin
);
plugins.inspector.registerView(MapView);
plugins.home.featureCatalogue.register(featureCatalogueEntry);
if (plugins.home) {
plugins.home.featureCatalogue.register(featureCatalogueEntry);
}
plugins.visualizations.registerAlias(
getMapsVisTypeAlias(plugins.visualizations, config.showMapVisualizationTypes)
);

View file

@ -91,6 +91,7 @@ export function getPluginPrivileges() {
admin: {
...privilege,
api: allMlCapabilitiesKeys.map((k) => `ml:${k}`),
catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`],
ui: allMlCapabilitiesKeys,
savedObject: {
all: savedObjects,
@ -100,6 +101,7 @@ export function getPluginPrivileges() {
user: {
...privilege,
api: userMlCapabilitiesKeys.map((k) => `ml:${k}`),
catalogue: [PLUGIN_ID],
ui: userMlCapabilitiesKeys,
savedObject: {
all: [],

View file

@ -10,7 +10,6 @@
"data",
"cloud",
"features",
"home",
"licensing",
"usageCollection",
"share",
@ -20,6 +19,7 @@
"indexPatternManagement"
],
"optionalPlugins": [
"home",
"security",
"spaces",
"management",
@ -32,6 +32,7 @@
"kibanaUtils",
"kibanaReact",
"dashboard",
"savedObjects"
"savedObjects",
"home"
]
}

View file

@ -50,7 +50,7 @@ export interface MlSetupDependencies {
management?: ManagementSetup;
usageCollection: UsageCollectionSetup;
licenseManagement?: LicenseManagementUIPluginSetup;
home: HomePublicPluginSetup;
home?: HomePublicPluginSetup;
embeddable: EmbeddableSetup;
uiActions: UiActionsSetup;
kibanaVersion: string;
@ -111,7 +111,9 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
const [coreStart] = await core.getStartServices();
if (isMlEnabled(license)) {
// add ML to home page
registerFeature(pluginsSetup.home);
if (pluginsSetup.home) {
registerFeature(pluginsSetup.home);
}
// register ML for the index pattern management no data screen.
pluginsSetup.indexPatternManagement.environment.update({

View file

@ -6,8 +6,8 @@
import { i18n } from '@kbn/i18n';
import {
HomePublicPluginSetup,
FeatureCatalogueCategory,
HomePublicPluginSetup,
} from '../../../../src/plugins/home/public';
import { PLUGIN_ID } from '../common/constants/app';
@ -28,7 +28,22 @@ export const registerFeature = (home: HomePublicPluginSetup) => {
}),
icon: 'machineLearningApp',
path: '/app/ml',
showOnHomePage: true,
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
});
home.featureCatalogue.register({
id: `${PLUGIN_ID}_file_data_visualizer`,
title: i18n.translate('xpack.ml.fileDataVisualizerTitle', {
defaultMessage: 'Upload a file',
}),
description: i18n.translate('xpack.ml.fileDataVisualizerDescription', {
defaultMessage: 'Import your own CSV, NDJSON, or log file.',
}),
icon: 'document',
path: '/app/ml#/filedatavisualizer',
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA,
order: 520,
});
};

View file

@ -87,7 +87,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
order: 500,
navLinkId: PLUGIN_ID,
app: [PLUGIN_ID, 'kibana'],
catalogue: [PLUGIN_ID],
catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`],
management: {
insightsAndAlerting: ['jobsListLink'],
},

View file

@ -54,14 +54,17 @@ export class MonitoringPlugin
if (home) {
home.featureCatalogue.register({
id,
title,
title: i18n.translate('xpack.monitoring.featureCatalogueTitle', {
defaultMessage: 'Monitor the stack',
}),
icon,
path: '/app/monitoring',
showOnHomePage: true,
category: FeatureCatalogueCategory.ADMIN,
description: i18n.translate('xpack.monitoring.monitoringDescription', {
defaultMessage: 'Track the real-time health and performance of your Elastic Stack.',
description: i18n.translate('xpack.monitoring.featureCatalogueDescription', {
defaultMessage: 'Track the real-time health and performance of your deployment.',
}),
order: 610,
});
}

View file

@ -7,7 +7,8 @@
"observability"
],
"optionalPlugins": [
"licensing"
"licensing",
"home"
],
"ui": true,
"server": true,

Some files were not shown because too many files have changed in this diff Show more