diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index 8cf9b9c656d8f..2f83ccf332dbd 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["management"], + "requiredPlugins": ["management", "home"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index d8853015d362a..dd4383d76a738 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -114,6 +114,19 @@ 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); + if (element) { + element.scrollIntoView(); + window.scrollBy(0, -48); // offsets scroll by height of the global nav + } + }, 0); + } } componentWillUnmount() { diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 32bfc0826e7c4..83b702ecef0e9 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -671,6 +671,7 @@ export class Field extends PureComponent<FieldProps> { return ( <EuiDescribedFormGroup + id={id} className={className} title={this.renderTitle(setting)} description={this.renderDescription(setting)} diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 35f6dd65925ba..f9066cdc9ec64 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -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,20 @@ export class AdvancedSettingsPlugin }, }); + if (home) { + home.featureCatalogue.register({ + id: 'advanced_settings', + title, + description: i18n.translate('xpack.advancedSettings.featureCatalogueTitle', { + defaultMessage: 'Customize your Kibana experience', + }), + icon: 'gear', // TODO: Do we want to use this icon here? + path: '/app/management/kibana/settings', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + } + return { component: component.setup, }; diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index a233b3debab8d..cc59f52b1f30f 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -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 }; diff --git a/src/plugins/home/common/constants.ts b/src/plugins/home/common/constants.ts new file mode 100644 index 0000000000000..a9457a9d3307c --- /dev/null +++ b/src/plugins/home/common/constants.ts @@ -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}`; diff --git a/src/plugins/home/public/application/components/_add_data.scss b/src/plugins/home/public/application/components/_add_data.scss index 836b34227a37c..115f957059475 100644 --- a/src/plugins/home/public/application/components/_add_data.scss +++ b/src/plugins/home/public/application/components/_add_data.scss @@ -1,63 +1,8 @@ -.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 { +.homAddData__image { 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; + .euiImage__img { + max-height: $euiSize * 16; // too tall if only two "Add data" features are enabled + width: auto; } } diff --git a/src/plugins/home/public/application/components/_home.scss b/src/plugins/home/public/application/components/_home.scss index 4101f6519829b..77d401d5af43e 100644 --- a/src/plugins/home/public/application/components/_home.scss +++ b/src/plugins/home/public/application/components/_home.scss @@ -1,3 +1,32 @@ +// Local page vars +$homePageHeaderHeight: $euiSize * 8; +$homePageWidth: 1200px; + +.homPageHeader { + height: $homePageHeaderHeight; + margin: 0 auto; + max-width: $homePageWidth; + padding: $euiSizeXL $euiSize 0; +} + +.homPageContainer { + min-height: calc(100vh - #{$homePageHeaderHeight}); + background-color: $euiColorEmptyShade; + border-top: 1px solid $euiColorLightShade; +} + +.homPage { + display: flex; + max-width: $homePageWidth; + margin: 0 auto; + padding: 0 $euiSize $euiSizeXL; + background-color: transparent; +} + +.homHome__synopsisItem { + max-width: 50%; +} + @include euiBreakpoint('xs', 's', 'm') { .homHome__synopsisItem { flex-basis: 100% !important; diff --git a/src/plugins/home/public/application/components/_index.scss b/src/plugins/home/public/application/components/_index.scss index 870099ffb350e..59bcdd0e8f289 100644 --- a/src/plugins/home/public/application/components/_index.scss +++ b/src/plugins/home/public/application/components/_index.scss @@ -8,6 +8,7 @@ @import 'add_data'; @import 'home'; @import 'sample_data_set_cards'; +@import 'solutions_panel'; @import 'synopsis'; @import 'welcome'; diff --git a/src/plugins/home/public/application/components/_solutions_panel.scss b/src/plugins/home/public/application/components/_solutions_panel.scss new file mode 100644 index 0000000000000..1b2d246818574 --- /dev/null +++ b/src/plugins/home/public/application/components/_solutions_panel.scss @@ -0,0 +1,100 @@ +.homSolutionsPanel { + margin-top: -$euiSizeXL; + min-height: $euiSize*16; + display: flex; + + .homSolutionsPanel--restrictHalfWidth { + max-width: 50%; + } + + .homSolutionsPanel__solutionPanel { + display: flex; + align-items: stretch; + + .homSolutionsPanel__solutionTitle { + padding: $euiSize; + + .euiToken { + padding: $euiSizeS; + } + } + + .homSolutionsPanel__CTA { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + padding: $euiSize; + } + } + + .homSolutionsPanel__header { + border-radius: $euiBorderRadius 0 0 $euiBorderRadius; + color: $euiColorEmptyShade; + + img { + position: absolute; + width: auto; + } + } + + .homSolutionsPanel__enterpriseSearch { + .homSolutionsPanel__enterpriseSearchHeader { + background-color: #017d73; + + .homSolutionsPanel__enterpriseSearchTopLeftImage img { + top: $euiSizeS; + left: 0; + height: $euiSizeXL; + } + + .homSolutionsPanel__enterpriseSearchBottomRightImage img { + right: $euiSizeS; + bottom: $euiSizeS; + height: $euiSizeXL; + } + } + } + + .homSolutionsPanel__observability { + .homSolutionsPanel__observabilityHeader { + background-color: #c42373; + + .homSolutionsPanel__observabilityTopRightImage img { + top: $euiSizeS; + right: $euiSizeS; + height: $euiSizeXL; + } + } + } + + .homSolutionsPanel__securitySolution { + .homSolutionsPanel__securitySolutionHeader { + background-color: #343741; + + .homSolutionsPanel__securitySolutionTopLeftImage img { + top: $euiSizeS; + left: $euiSizeS; + height: $euiSizeXXL; + } + } + } + + .homSolutionsPanel__kibana { + .homSolutionsPanel__kibanaHeader { + background-color: #006bb4; + + .homSolutionsPanel__kibanaTopLeftImage img { + top: 0; + left: 0; + height: $euiSizeXXL * 4; + } + + .homSolutionsPanel__kibanaBottomRightImage img { + right: 0; + bottom: 0; + height: $euiSizeXXL * 4; + } + } + } +} diff --git a/src/plugins/home/public/application/components/_synopsis.scss b/src/plugins/home/public/application/components/_synopsis.scss index 49e71f159fe6f..3eac2bc9705e0 100644 --- a/src/plugins/home/public/application/components/_synopsis.scss +++ b/src/plugins/home/public/application/components/_synopsis.scss @@ -5,6 +5,10 @@ box-shadow: none; } + .homSynopsis__cardTitle { + display: flex; + } + // SASSTODO: Fix in EUI .euiCard__content { padding-top: 0 !important; diff --git a/src/plugins/home/public/application/components/add_data.js b/src/plugins/home/public/application/components/add_data.js deleted file mode 100644 index c35b7b04932fb..0000000000000 --- a/src/plugins/home/public/application/components/add_data.js +++ /dev/null @@ -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); diff --git a/src/plugins/home/public/application/components/add_data.test.js b/src/plugins/home/public/application/components/add_data.test.js deleted file mode 100644 index 9457f766409b8..0000000000000 --- a/src/plugins/home/public/application/components/add_data.test.js +++ /dev/null @@ -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); -}); diff --git a/src/plugins/home/public/application/components/app_navigation_handler.ts b/src/plugins/home/public/application/components/app_navigation_handler.ts index 6e78af7f42f52..61d85c033b544 100644 --- a/src/plugins/home/public/application/components/app_navigation_handler.ts +++ b/src/plugins/home/public/application/components/app_navigation_handler.ts @@ -17,6 +17,7 @@ * under the License. */ +import { MouseEvent } from 'react'; import { getServices } from '../kibana_services'; export const createAppNavigationHandler = (targetUrl: string) => (event: MouseEvent) => { diff --git a/src/plugins/home/public/application/components/change_home_route/change_home_route.test.tsx b/src/plugins/home/public/application/components/change_home_route/change_home_route.test.tsx new file mode 100644 index 0000000000000..9880b336e76e5 --- /dev/null +++ b/src/plugins/home/public/application/components/change_home_route/change_home_route.test.tsx @@ -0,0 +1,18 @@ +/* + * 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. + */ diff --git a/src/plugins/home/public/application/components/change_home_route/change_home_route.tsx b/src/plugins/home/public/application/components/change_home_route/change_home_route.tsx new file mode 100644 index 0000000000000..1dc1bcc60ae4c --- /dev/null +++ b/src/plugins/home/public/application/components/change_home_route/change_home_route.tsx @@ -0,0 +1,65 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { HOME_APP_BASE_PATH } from '../../../../common/constants'; +import { getServices } from '../../kibana_services'; +import { createAppNavigationHandler } from '../app_navigation_handler'; + +interface Props { + defaultRoute?: string; +} + +export const ChangeHomeRoute: FunctionComponent<Props> = ({ defaultRoute }) => { + const { uiSettings } = getServices(); + const changeDefaultRoute = () => uiSettings.set('defaultRoute', defaultRoute); + + return ( + <EuiFlexGroup className="homPage__footer" alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiText size="s" color="subdued"> + <p> + <FormattedMessage + id="home.changeHomeRouteText" + defaultMessage="Would you prefer to have an alternate home page for this Elastic space? " + /> + </p> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + iconType="home" + // onClick={changeDefaultRoute} + onClick={createAppNavigationHandler('/app/management/kibana/settings#defaultRoute')} + > + <FormattedMessage + id="home.changeHomeRouteLink" + defaultMessage="Change your home page route" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +ChangeHomeRoute.defaultProps = { + defaultRoute: HOME_APP_BASE_PATH, +}; diff --git a/src/plugins/home/public/application/components/change_home_route/index.tsx b/src/plugins/home/public/application/components/change_home_route/index.tsx new file mode 100644 index 0000000000000..430100a29b161 --- /dev/null +++ b/src/plugins/home/public/application/components/change_home_route/index.tsx @@ -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 './change_home_route'; diff --git a/src/plugins/home/public/application/components/home.js b/src/plugins/home/public/application/components/home.js index f8769bfd0d618..f44fb914c7fda 100644 --- a/src/plugins/home/public/application/components/home.js +++ b/src/plugins/home/public/application/components/home.js @@ -17,30 +17,27 @@ * under the License. */ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { Synopsis } from './synopsis'; -import { AddData } from './add_data'; +import { ChangeHomeRoute } from './change_home_route'; +import { SolutionsPanel } from './solutions_panel'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiButton, - EuiPage, - EuiPanel, + EuiButtonEmpty, EuiTitle, - EuiSpacer, EuiFlexGroup, EuiFlexItem, - EuiFlexGrid, - EuiText, - EuiPageBody, EuiScreenReaderOnly, + EuiSpacer, + EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Welcome } from './welcome'; import { getServices } from '../kibana_services'; -import { FeatureCatalogueCategory } from '../../services'; +import { HOME_APP_BASE_PATH } from '../../../common/constants'; import { createAppNavigationHandler } from './app_navigation_handler'; const KEY_ENABLE_WELCOME = 'home:welcome:show'; @@ -116,105 +113,204 @@ 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.showOnHomePage && directory.id === id); + + renderDirectory = (directory) => { + const { addBasePath } = this.props; + + return directory ? ( + <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)} + wrapInPanel + /> + </EuiFlexItem> + ) : null; }; renderNormal() { - const { apmUiEnabled, mlEnabled } = this.props; + const { addBasePath, directories } = this.props; + + const fileDataVisualizer = this.findDirectoryById('ml_file_data_visualizer'); + const ingestManager = this.findDirectoryById('ingestManager'); + const security = this.findDirectoryById('security'); + const monitoring = this.findDirectoryById('monitoring'); + const snapshotRestore = this.findDirectoryById('snapshot_restore'); + const indexLifecycleManagement = this.findDirectoryById('index_lifecycle_management'); + const devTools = this.findDirectoryById('console'); + const stackManagement = this.findDirectoryById('stack-management'); + const advancedSettings = this.findDirectoryById('advanced_settings'); + + console.log({ directories }); 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> + <Fragment> + <header className="homPageHeader"> + <EuiFlexGroup gutterSize="none"> <EuiFlexItem> - <EuiPanel paddingSize="l"> - <EuiTitle size="s"> - <h2> + <EuiScreenReaderOnly> + <h1> + <FormattedMessage id="home.welcomeHomePageHeader" defaultMessage="Kibana home" /> + </h1> + </EuiScreenReaderOnly> + <EuiFlexGroup gutterSize="none"> + <EuiTitle size="m"> + <h1> <FormattedMessage - id="home.directories.visualize.nameTitle" - defaultMessage="Visualize and Explore Data" + id="home.pageHeader.welcomeNoUserTitle" + defaultMessage="Welcome to {ELASTIC}!" + values={{ ELASTIC: 'Elastic' }} /> - </h2> + </h1> </EuiTitle> - <EuiSpacer size="m" /> - <EuiFlexGrid columns={2} gutterSize="s"> - {this.renderDirectories(FeatureCatalogueCategory.DATA)} - </EuiFlexGrid> - </EuiPanel> + </EuiFlexGroup> </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> + <EuiFlexGroup alignItems="flexEnd"> + <EuiFlexItem> + <EuiButtonEmpty href="#/tutorial_directory" iconType="plusInCircle"> + {i18n.translate('home.pageHeader.addDataButtonLabel', { + defaultMessage: 'Add data', + })} + </EuiButtonEmpty> + </EuiFlexItem> + {stackManagement ? ( + <EuiFlexItem> + <EuiButtonEmpty + onClick={createAppNavigationHandler(stackManagement.path)} + iconType="gear" + > + {i18n.translate('home.pageHeader.stackManagementButtonLabel', { + defaultMessage: 'Manage stack', + })} + </EuiButtonEmpty> + </EuiFlexItem> + ) : null} + {devTools ? ( + <EuiFlexItem> + <EuiButtonEmpty + onClick={createAppNavigationHandler(devTools.path)} + iconType="wrench" + > + {i18n.translate('home.pageHeader.devToolsButtonLabel', { + defaultMessage: 'Dev tools', + })} + </EuiButtonEmpty> + </EuiFlexItem> + ) : null} + {/* <EuiFlexItem> + <EuiButtonEmpty href="#/feature_directory" iconType="apps"> + {i18n.translate('home.pageHeader.appDirectoryButtonLabel', { + defaultMessage: 'App directory', + })} + </EuiButtonEmpty> + </EuiFlexItem> */} + </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> + </header> + <div className="homPageContainer"> + <main className="homPage" data-test-subj="homeApp"> + <div className="homSolutionsPanel"> + <SolutionsPanel + addBasePath={addBasePath} + findDirectoryById={this.findDirectoryById} + /> + </div> - <EuiSpacer size="l" /> - - <EuiFlexGroup justifyContent="center"> - <EuiFlexItem grow={false} className="eui-textCenter"> - <EuiText size="s" color="subdued"> - <p> - <FormattedMessage - id="home.directories.notFound.description" - defaultMessage="Didn’t find what you were looking for?" - /> - </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> + <EuiHorizontalRule margin="xl" /> + <EuiSpacer size="s" /> + + <div className="homAddData"> + <EuiFlexGroup justifyContent="spaceBetween" alignItems="baseline"> + <EuiFlexItem grow={1}> + <EuiTitle size="s"> + <h3> + {i18n.translate('home.addData.sectionTitle', { + defaultMessage: 'Add your data', + })} + </h3> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + iconType="tableDensityExpanded" + href="#/tutorial_directory/sampleData" + size="xs" + > + <FormattedMessage + id="home.addData.sampleDataButtonLabel" + defaultMessage="Try our sample data" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer /> + + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <EuiFlexGroup justifyContent="spaceAround"> + <EuiFlexItem className="homHome__synopsisItem"> + <Synopsis + description={i18n.translate('home.addData.addIntegrationDescription', { + defaultMessage: 'Add data from a variety of common sources.', + })} + iconType="indexOpen" + title={i18n.translate('home.addData.addIntegrationTitle', { + defaultMessage: 'Add an integration', + })} + url="#/tutorial_directory" + wrapInPanel + /> + </EuiFlexItem> + {this.renderDirectory(ingestManager)} + {this.renderDirectory(fileDataVisualizer)} + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </div> + + {security || monitoring || snapshotRestore || indexLifecycleManagement ? ( + <Fragment> + <EuiHorizontalRule margin="xl" /> + <EuiSpacer size="s" /> + + <div className="homManageData"> + <EuiTitle size="s"> + <h3> + {i18n.translate('home.manageData.sectionTitle', { + defaultMessage: 'Manage your data', + })} + </h3> + </EuiTitle> + + <EuiSpacer /> + + <EuiFlexGroup justifyContent="spaceAround"> + {this.renderDirectory(security)} + {this.renderDirectory(monitoring)} + {this.renderDirectory(snapshotRestore)} + {this.renderDirectory(indexLifecycleManagement)} + </EuiFlexGroup> + </div> + </Fragment> + ) : null} + + {advancedSettings && ( + <Fragment> + <EuiHorizontalRule margin="xl" /> + <ChangeHomeRoute defaultRoute={HOME_APP_BASE_PATH} /> + </Fragment> + )} + </main> + </div> + </Fragment> ); } @@ -262,11 +358,9 @@ Home.propTypes = { category: PropTypes.string.isRequired, }) ), - 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, diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 648915b6dae0c..53f094943480d 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -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,6 @@ export function HomeApp({ directories }) { <Home addBasePath={addBasePath} directories={directories} - apmUiEnabled={apmUiEnabled} - mlEnabled={mlEnabled} find={savedObjectsClient.find} localStorage={localStorage} urlBasePath={getBasePath()} diff --git a/src/plugins/home/public/application/components/solutions_panel/index.ts b/src/plugins/home/public/application/components/solutions_panel/index.ts new file mode 100644 index 0000000000000..171e677403dd1 --- /dev/null +++ b/src/plugins/home/public/application/components/solutions_panel/index.ts @@ -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_panel'; diff --git a/src/plugins/home/public/application/components/solutions_panel/solutions_panel.tsx b/src/plugins/home/public/application/components/solutions_panel/solutions_panel.tsx new file mode 100644 index 0000000000000..a0724717ff22b --- /dev/null +++ b/src/plugins/home/public/application/components/solutions_panel/solutions_panel.tsx @@ -0,0 +1,433 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import { SolutionsTitle } from './solutions_title'; +import { FeatureCatalogueEntry } from '../../../'; +import { createAppNavigationHandler } from '../app_navigation_handler'; + +interface Props { + addBasePath: (path: string) => string; + findDirectoryById: (id: string) => FeatureCatalogueEntry; +} + +// TODO: Bolding the first word/verb won't look write in other languages +const getActionText = ({ verb, text }: { verb: string; text: string }): JSX.Element => ( + <EuiText size="s" key={`${verb} ${text}`}> + <p> + <strong>{verb}</strong> {text} + </p> + </EuiText> +); + +// TODO: Should this live here? Should it be registered per app? +const solutionCTAs: { [key: string]: any } = { + enterpriseSearch: { + websiteSearch: { + verb: i18n.translate('home.solutionsPanel.enterpriseSearch.firstActionVerb', { + defaultMessage: 'Build', + description: + 'The first word of this sentence is bolded. Full sentence: "Build a powerful website search."', + }), + text: i18n.translate('home.solutionsPanel.enterpriseSearch.firstActionText', { + defaultMessage: 'a powerful website search.', + description: 'Full sentence: "Build a powerful website search."', + }), + }, + appSearch: { + verb: i18n.translate('home.solutionsPanel.enterpriseSearch.secondActionVerb', { + defaultMessage: 'Search', + description: + 'The first word of this sentence is bolded. Full sentence: "Search any data from any application."', + }), + text: i18n.translate('home.solutionsPanel.enterpriseSearch.secondActionText', { + defaultMessage: 'any data from any application.', + description: 'Full sentence: "Search any data from any application."', + }), + }, + workplaceSearch: { + verb: i18n.translate('home.solutionsPanel.enterpriseSearch.thirdActionVerb', { + defaultMessage: 'Unify', + description: + 'The first word of this sentence is bolded. Full sentence: "Unify searchable workplace content."', + }), + text: i18n.translate('home.solutionsPanel.enterpriseSearch.thirdActionText', { + defaultMessage: 'searchable workplace content.', + description: 'Full sentence: "Unify searchable workplace content."', + }), + }, + }, + observability: { + metrics: { + verb: i18n.translate('home.solutionsPanel.observability.firstActionVerb', { + defaultMessage: 'Monitor', + description: + 'The first word of this sentence is bolded. Full sentence: "Monitor all infrastructure metrics."', + }), + text: i18n.translate('home.solutionsPanel.observability.firstActionText', { + defaultMessage: 'all infrastructure metrics.', + description: 'Full sentence: "Monitor all infrastructure metrics."', + }), + }, + apm: { + verb: i18n.translate('home.solutionsPanel.observability.secondActionVerb', { + defaultMessage: 'Track', + description: + 'The first word of this sentence is bolded. Full sentence: "Track application performance."', + }), + text: i18n.translate('home.solutionsPanel.observability.secondActionText', { + defaultMessage: 'application performance.', + description: 'Full sentence: "Track application performance."', + }), + }, + uptime: { + verb: i18n.translate('home.solutionsPanel.observability.thirdActionVerb', { + defaultMessage: 'Measure', + description: + 'The first word of the following sentence is bolded. Full sentence: "Measure SLAs and react to issues."', + }), + text: i18n.translate('home.solutionsPanel.observability.thirdActionText', { + defaultMessage: 'SLAs and react to issues.', + description: 'Full sentence: "Measure SLAs and react to issues."', + }), + }, + }, + securitySolution: [ + { + verb: i18n.translate('home.solutionsPanel.securitySolution.firstActionVerb', { + defaultMessage: 'Detect', + description: + 'The first word of this sentence is bolded. Full sentence: "Detect critical security events."', + }), + text: i18n.translate('home.solutionsPanel.securitySolution.firstActionText', { + defaultMessage: 'critical security events.', + description: 'Full sentence: "Detect critical security events."', + }), + }, + { + verb: i18n.translate('home.solutionsPanel.securitySolution.secondActionVerb', { + defaultMessage: 'Investigate', + description: + 'The first word of this sentence is bolded. Full sentence: "Investigate incidents and collaborate."', + }), + text: i18n.translate('home.solutionsPanel.securitySolution.secondActionText', { + defaultMessage: 'incidents and collaborate.', + description: 'Full sentence: "Investigate incidents and collaborate."', + }), + }, + { + verb: i18n.translate('home.solutionsPanel.securitySolution.thirdActionVerb', { + defaultMessage: 'Prevent', + description: + 'The first word of the following sentence is bolded. Full sentence: "Prevent threats autonomously."', + }), + text: i18n.translate('home.solutionsPanel.securitySolution.thirdActionText', { + defaultMessage: 'threats autonomously.', + description: 'Full sentence: "Prevent threats autonomously."', + }), + }, + ], + kibana: { + dashboard: { + verb: i18n.translate('home.solutionsPanel.kibana.dashboardVerb', { + defaultMessage: 'Visualize', + description: + 'The first word of this sentence is bolded. Full sentence: "Visualize every aspect of your data."', + }), + text: i18n.translate('home.solutionsPanel.kibana.dashboardText', { + defaultMessage: 'every aspect of your data.', + description: 'Full sentence: "Visualize every aspect of your data."', + }), + }, + discover: { + verb: i18n.translate('home.solutionsPanel.kibana.discoverVerb', { + defaultMessage: 'Search', + description: + 'The first word of this sentence is bolded. Full sentence: "Search and explore your data."', + }), + text: i18n.translate('home.solutionsPanel.kibana.discoverText', { + defaultMessage: 'and explore your data.', + description: 'Full sentence: "Search and explore your data."', + }), + }, + canvas: { + verb: i18n.translate('home.solutionsPanel.kibana.fourthActionVerb', { + defaultMessage: 'Craft', + description: + 'The first word of this sentence is bolded. Full sentence: "Craft pixel-perfect reports."', + }), + text: i18n.translate('home.solutionsPanel.kibana.fourthActionText', { + defaultMessage: 'pixel-perfect reports.', + description: 'Full sentence: "Craft pixel-perfect reports."', + }), + }, + maps: { + verb: i18n.translate('home.solutionsPanel.kibana.thirdActionVerb', { + defaultMessage: 'Plot', + description: + 'The first word of the following sentence is bolded. Full sentence: "Plot your geographic information."', + }), + text: i18n.translate('home.solutionsPanel.kibana.thirdActionText', { + defaultMessage: 'your geographic information.', + description: 'Full sentence: "Plot your geographic information."', + }), + }, + ml: { + verb: i18n.translate('home.solutionsPanel.kibana.fifthActionVerb', { + defaultMessage: 'Detect', + description: + 'The first word of this sentence is bolded. Full sentence: "Detect anomalous events."', + }), + text: i18n.translate('home.solutionsPanel.kibana.fifthActionText', { + defaultMessage: 'anomalous events.', + description: 'Full sentence: "Detect anomalous events."', + }), + }, + graph: { + verb: i18n.translate('home.solutionsPanel.kibana.sixthActionVerb', { + defaultMessage: 'Reveal', + description: + 'The first word of the following sentence is bolded. Full sentence: "Reveal patterns and relationships."', + }), + text: i18n.translate('home.solutionsPanel.kibana.sixthActionText', { + defaultMessage: 'patterns and relationships.', + description: 'Full sentence: "Reveal patterns and relationships."', + }), + }, + }, +}; + +const halfWidthClass = 'homeSolutionsPanel--restrictHalfWidth'; + +export const SolutionsPanel: FunctionComponent<Props> = ({ + addBasePath, + findDirectoryById +}) => { + const observability = findDirectoryById('observability'); + const enterpriseSearch = findDirectoryById('appSearch'); + const securitySolution = findDirectoryById('securitySolution'); + + + const addSpacersBetweenReducer = ( + acc: JSX.Element[], + element: JSX.Element, + index: number, + elements: JSX.Element[] + ) => { + acc.push(element); + if (index < elements.length - 1) { + acc.push(<EuiSpacer key={`homeSolutionsPanel__CTASpacer${index}`} size="m" />); + } + return acc; + }; + + const getActionsBySolution = (solution: string, appIds: string[]): JSX.Element[] => + appIds.reduce<JSX.Element[]>((acc: JSX.Element[], appId: string, index: number) => { + const directory = findDirectoryById(appId); + if (directory) { + const CTA = solutionCTAs[solution][appId] as { verb: string; text: string }; + if (CTA) { + // acc.push(directory.description); + acc.push(getActionText(CTA)); + } + } + return acc; + }, [] as JSX.Element[]); + + const enterpriseSearchAppIds = ['webSearch', 'appSearch', 'workplaceSearch']; + const observabilityAppIds = ['metrics', 'apm', 'uptime']; + const kibanaAppIds = ['dashboard', 'discover', 'canvas', 'maps', 'ml', 'graph']; + + const enterpriseSearchActions = getActionsBySolution( + 'enterpriseSearch', + enterpriseSearchAppIds + ).reduce(addSpacersBetweenReducer, []); + const observabilityActions = getActionsBySolution('observability', observabilityAppIds).reduce( + addSpacersBetweenReducer, + [] + ); + const securitySolutionActions = solutionCTAs.securitySolution + .map(getActionText) + .reduce(addSpacersBetweenReducer, []); + const kibanaActions = getActionsBySolution('kibana', kibanaAppIds).reduce( + addSpacersBetweenReducer, + [] + ); + +const halfWidthClass = 'homSolutionsPanel--restrictHalfWidth'; + + return ( + <EuiFlexGroup justifyContent="spaceAround"> + {enterpriseSearchActions.length || observabilityActions.length || securitySolution ? ( + + <EuiFlexItem className={halfWidthClass}> + {/* TODO: once app search is merged, register add to feature catalogue and remove hard coded text here */} + <EuiFlexGroup direction="column"> + {enterpriseSearchActions.length ? ( + + <EuiFlexItem className="homSolutionsPanel__enterpriseSearch"> + <EuiPanel + paddingSize="none" + className="homSolutionsPanel__solutionPanel" + onClick={createAppNavigationHandler('/app/enterprise_search/app_search')} // TODO: double check this url once enterprise search overview page is available + > + <EuiFlexGroup gutterSize="none"> + <EuiFlexItem + grow={1} + className="homSolutionsPanel__header homSolutionsPanel__enterpriseSearchHeader" + > + <EuiImage + className="homSolutionsPanel__enterpriseSearchTopLeftImage" + url={addBasePath( + '/plugins/home/assets/background_enterprise_search_top_left_2x.png' + )} + alt="Enterprise search top left background graphic" + /> + <SolutionsTitle + iconType="logoEnterpriseSearch" + title="Enterprise Search" + subtitle={i18n.translate('home.solutionsPanel.enterpriseSearchSubtitle', { + defaultMessage: 'Search everything', + })} + /> + <EuiImage + className="homSolutionsPanel__enterpriseSearchBottomRightImage" + url={addBasePath( + '/plugins/home/assets/background_enterprise_search_bottom_right_2x.png' + )} + alt="Enterprise search bottom right background graphic" + /> + </EuiFlexItem> + <EuiFlexItem grow={1} className="homSolutionsPanel__CTA"> + {enterpriseSearchActions} + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </EuiFlexItem> + ) : null} + {observabilityActions.length ? ( + + <EuiFlexItem className="homSolutionsPanel__observability"> + <EuiPanel + paddingSize="none" + className="homSolutionsPanel__solutionPanel" + // onClick={createAppNavigationHandler(observability.path)} + onClick={createAppNavigationHandler('/app/observability#/landing')} + + > + <EuiFlexGroup gutterSize="none"> + <EuiFlexItem + grow={1} + className="homSolutionsPanel__header homSolutionsPanel__observabilityHeader" + > + <EuiImage + className="homSolutionsPanel__observabilityTopRightImage" + url={addBasePath( + '/plugins/home/assets/background_observability_top_right_2x.png' + )} + alt="Observability top right background graphic" + /> + <SolutionsTitle + iconType={observability.icon} + title={observability.title} + subtitle={observability.description} + /> + </EuiFlexItem> + <EuiFlexItem grow={1} className="homSolutionsPanel__CTA"> + {observabilityActions} + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </EuiFlexItem> + ) : null} + {securitySolution ? ( + <EuiFlexItem className="homSolutionsPanel__securitySolution"> + <EuiPanel + paddingSize="none" + className="homSolutionsPanel__solutionPanel" + onClick={createAppNavigationHandler(securitySolution.path)} + > + <EuiFlexGroup gutterSize="none"> + <EuiFlexItem + grow={1} + className="homSolutionsPanel__header homSolutionsPanel__securitySolutionHeader" + > + <EuiImage + className="homSolutionsPanel__securitySolutionTopLeftImage" + url={addBasePath( + '/plugins/home/assets/background_security_solution_top_left_2x.png' + )} + alt="Enterprise search top left background graphic" + /> + <SolutionsTitle + iconType={securitySolution.icon} + title={securitySolution.title} + subtitle={securitySolution.description} + /> + </EuiFlexItem> + <EuiFlexItem grow={1} className="homSolutionsPanel__CTA"> + {securitySolutionActions} + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </EuiFlexItem> + ) : null} + </EuiFlexGroup> + </EuiFlexItem> + ) : null} +{kibanaActions.length ? ( + <EuiFlexItem className={`homSolutionsPanel__kibana ${halfWidthClass}`}> + <EuiPanel + paddingSize="none" + className="homSolutionsPanel__solutionPanel" + onClick={createAppNavigationHandler('/app/dashboards')} + > + <EuiFlexGroup gutterSize="none"> + <EuiFlexItem + grow={1} + className="homSolutionsPanel__header homSolutionsPanel__kibanaHeader" + > + <EuiImage + className="homSolutionsPanel__kibanaTopLeftImage" + url={addBasePath('/plugins/home/assets/background_kibana_top_left_2x.png')} + alt="Kibana top left background graphic" + /> + <SolutionsTitle + iconType="logoKibana" + title="Kibana" + subtitle={i18n.translate('home.solutionsPanel.kibanaSubtitle', { + defaultMessage: 'Visualize & analyze', + })} + /> + <EuiImage + className="homSolutionsPanel__kibanaBottomRightImage" + url={addBasePath('/plugins/home/assets/background_kibana_bottom_right_2x.png')} + alt="Kibana bottom right background graphic" + /> + </EuiFlexItem> + <EuiFlexItem grow={1} className="homSolutionsPanel__CTA"> + {kibanaActions} + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </EuiFlexItem>):null} + </EuiFlexGroup> +);} diff --git a/src/plugins/home/public/application/components/solutions_panel/solutions_title.tsx b/src/plugins/home/public/application/components/solutions_panel/solutions_title.tsx new file mode 100644 index 0000000000000..3503edf885044 --- /dev/null +++ b/src/plugins/home/public/application/components/solutions_panel/solutions_title.tsx @@ -0,0 +1,53 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiToken, + EuiTitle, + EuiText, + EuiIcon, + IconType, +} from '@elastic/eui'; + +interface Props { + title: string; + subtitle: string; + iconType: IconType; +} + +export const SolutionsTitle: FunctionComponent<Props> = ({ title, subtitle, iconType }) => ( + <EuiFlexGroup gutterSize="none" alignItems="center" className="homSolutionsPanel__solutionTitle"> + <EuiFlexItem className="eui-textCenter"> + <p> + <EuiToken iconType={iconType} shape="circle" fill="light" size="l" /> + </p> + <EuiTitle className="eui-textInheritColor" size="s"> + <p>{title}</p> + </EuiTitle> + <EuiText size="s"> + <p> + {subtitle} <EuiIcon type="sortRight" /> + </p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> +); diff --git a/src/plugins/home/public/assets/background_enterprise_search_bottom_right_2x.png b/src/plugins/home/public/assets/background_enterprise_search_bottom_right_2x.png new file mode 100644 index 0000000000000..7f2037d3afb69 Binary files /dev/null and b/src/plugins/home/public/assets/background_enterprise_search_bottom_right_2x.png differ diff --git a/src/plugins/home/public/assets/background_enterprise_search_top_left_2x.png b/src/plugins/home/public/assets/background_enterprise_search_top_left_2x.png new file mode 100644 index 0000000000000..3e6e016bd73a7 Binary files /dev/null and b/src/plugins/home/public/assets/background_enterprise_search_top_left_2x.png differ diff --git a/src/plugins/home/public/assets/background_kibana_bottom_right_2x.png b/src/plugins/home/public/assets/background_kibana_bottom_right_2x.png new file mode 100644 index 0000000000000..14eedf77c649b Binary files /dev/null and b/src/plugins/home/public/assets/background_kibana_bottom_right_2x.png differ diff --git a/src/plugins/home/public/assets/background_kibana_top_left_2x.png b/src/plugins/home/public/assets/background_kibana_top_left_2x.png new file mode 100644 index 0000000000000..e7008e546138d Binary files /dev/null and b/src/plugins/home/public/assets/background_kibana_top_left_2x.png differ diff --git a/src/plugins/home/public/assets/background_observability_top_right_2x.png b/src/plugins/home/public/assets/background_observability_top_right_2x.png new file mode 100644 index 0000000000000..860491733ab65 Binary files /dev/null and b/src/plugins/home/public/assets/background_observability_top_right_2x.png differ diff --git a/src/plugins/home/public/assets/background_security_solution_top_left_2x.png b/src/plugins/home/public/assets/background_security_solution_top_left_2x.png new file mode 100644 index 0000000000000..5cb0358d02f02 Binary files /dev/null and b/src/plugins/home/public/assets/background_security_solution_top_left_2x.png differ diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index dc48332e052de..6079449554b08 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -24,6 +24,7 @@ export { EnvironmentSetup, TutorialSetup, HomePublicPluginSetup, + HomePublicPluginStart, } from './plugin'; export { FeatureCatalogueEntry, diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 6859d916a61af..ec272c3f38399 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -42,6 +42,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; @@ -56,7 +57,12 @@ export interface HomePluginSetupDependencies { export class HomePublicPlugin implements - Plugin<HomePublicPluginSetup, void, HomePluginSetupDependencies, HomePluginStartDependencies> { + Plugin< + HomePublicPluginSetup, + HomePublicPluginStart, + HomePluginSetupDependencies, + HomePluginStartDependencies + > { private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry(); private readonly environmentService = new EnvironmentService(); private readonly tutorialService = new TutorialService(); @@ -66,9 +72,9 @@ export class HomePublicPlugin public setup( core: CoreSetup<HomePluginStartDependencies>, { kibanaLegacy, usageCollection }: HomePluginSetupDependencies - ): HomePublicPluginSetup { + ) { core.application.register({ - id: 'home', + id: PLUGIN_ID, title: 'Home', navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { @@ -120,11 +126,9 @@ export class HomePublicPlugin { application: { capabilities, currentAppId$ }, http }: CoreStart, { kibanaLegacy }: HomePluginStartDependencies ) { - this.featuresCatalogueRegistry.start({ capabilities }); - // 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... @@ -136,6 +140,8 @@ export class HomePublicPlugin } }); } + + return { featureCatalogue: { ...this.featuresCatalogueRegistry.start({ capabilities }) } }; } } @@ -149,13 +155,5 @@ export type EnvironmentSetup = EnvironmentServiceSetup; export type TutorialSetup = TutorialServiceSetup; /** @public */ -export interface HomePublicPluginSetup { - tutorials: TutorialServiceSetup; - featureCatalogue: FeatureCatalogueSetup; - /** - * The environment service is only available for a transition period and will - * be replaced by display specific extension points. - * @deprecated - */ - environment: EnvironmentSetup; -} +export type HomePublicPluginSetup = ReturnType<HomePublicPlugin['setup']>; +export type HomePublicPluginStart = ReturnType<HomePublicPlugin['start']>; diff --git a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts index c70fc0464b131..d4e9b35d5fa43 100644 --- a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts +++ b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts @@ -42,7 +42,7 @@ export interface FeatureCatalogueEntry { /** URL path to link to this future. Should not include the basePath. */ readonly path: string; /** Whether or not this link should be shown on the front page of Kibana. */ - readonly showOnHomePage: boolean; + showOnHomePage: boolean; } export class FeatureCatalogueRegistry { @@ -65,6 +65,22 @@ export class FeatureCatalogueRegistry { public start({ capabilities }: { capabilities: Capabilities }) { this.capabilities = capabilities; + return { + showOnHomePage: (featureId: string) => { + const feature = this.features.get(featureId); + if (feature) { + feature.showOnHomePage = true; + this.features.set(featureId, feature); + } + }, + hideFromHomePage: (featureId: string) => { + const feature = this.features.get(featureId); + if (feature) { + feature.showOnHomePage = false; + this.features.set(featureId, feature); + } + }, + }; } public get(): readonly FeatureCatalogueEntry[] { diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 8be130b4028c6..93a5e36679c0d 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -56,7 +56,7 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart }), icon: 'managementApp', path: '/app/management', - showOnHomePage: false, + showOnHomePage: true, category: FeatureCatalogueCategory.ADMIN, }); diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index f264ae6cd9852..d31539f4bb6a0 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -75,6 +75,7 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> { const config = this.initializerContext.config.get(); const pluginSetupDeps = plugins; + // TODO: Can this be removed now with homepage redesign work, or would it be a breaking change? APM is no longer displayed on the new home page design pluginSetupDeps.home.environment.update({ apmUi: true }); pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index 5b2566ffab7c0..1c36d09069c6d 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -23,6 +23,7 @@ import { checkLicense } from '../common/check_license'; import { FeatureCatalogueCategory, HomePublicPluginSetup, + HomePublicPluginStart, } from '../../../../src/plugins/home/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { ConfigSchema } from '../config'; @@ -38,6 +39,7 @@ export interface GraphPluginStartDependencies { data: DataPublicPluginStart; savedObjects: SavedObjectsStart; kibanaLegacy: KibanaLegacyStart; + home?: HomePublicPluginStart; } export class GraphPlugin @@ -108,12 +110,20 @@ export class GraphPlugin }); } - start(core: CoreStart) { + start(core: CoreStart, { home }: GraphPluginStartDependencies) { if (this.licensing === null) { 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); + if (home) { + if (licenseInformation.showAppLink) { + home.featureCatalogue.showOnHomePage('graph'); + } else { + home.featureCatalogue.hideFromHomePage('graph'); + } + } }); } diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 1d26aa53752a9..b93ce200ce19a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -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); @@ -67,6 +68,21 @@ export class IndexLifecycleManagementPlugin { }, }); + home.featureCatalogue.register({ + id: PLUGIN.ID, + title: i18n.translate('xpack.indexLifecycleManagement.featureCatalogueTitle', { + defaultMessage: 'Manage index lifecycles', + }), + description: i18n.translate('xpack.indexLifecycleManagement.featureCatalogueTitle', { + defaultMessage: + 'Attach a policy to automate when and how to transition an index through its lifecycle.', + }), + icon: 'indexSettings', // TODO: This is the same icon used for rollups in the feature catalogue. Do we need to pick a different one here? + path: '/app/management/data/index_lifecycle_management', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + if (indexManagement) { addAllExtensions(indexManagement.extensionsService); } diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index 178884a7ee679..beb7753f32592 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -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 { diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index fa228e03194a9..3b5080ccdd3fe 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -15,11 +15,11 @@ export const METRICS_FEATURE = { icon: 'metricsApp', navLinkId: 'metrics', app: ['infra', 'kibana'], - catalogue: ['infraops'], + catalogue: ['infraops', 'metrics'], privileges: { all: { app: ['infra', 'kibana'], - catalogue: ['infraops'], + catalogue: ['infraops', 'metrics'], api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], savedObject: { all: ['infrastructure-ui-source', 'alert', 'action', 'action_task_params'], @@ -39,7 +39,7 @@ export const METRICS_FEATURE = { }, read: { app: ['infra', 'kibana'], - catalogue: ['infraops'], + catalogue: ['infraops', 'metrics'], api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], savedObject: { all: ['alert', 'action', 'action_task_params'], @@ -67,11 +67,11 @@ export const LOGS_FEATURE = { icon: 'logsApp', navLinkId: 'logs', app: ['infra', 'kibana'], - catalogue: ['infralogging'], + catalogue: ['infralogging', 'logs'], privileges: { all: { app: ['infra', 'kibana'], - catalogue: ['infralogging'], + catalogue: ['infralogging', 'logs'], api: ['infra'], savedObject: { all: ['infrastructure-ui-source'], @@ -81,7 +81,7 @@ export const LOGS_FEATURE = { }, read: { app: ['infra', 'kibana'], - catalogue: ['infralogging'], + catalogue: ['infralogging', 'logs'], api: ['infra'], savedObject: { all: [], diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json index ab0a2ba24ba66..7bbbcde027fd4 100644 --- a/x-pack/plugins/ingest_manager/kibana.json +++ b/x-pack/plugins/ingest_manager/kibana.json @@ -4,8 +4,8 @@ "server": true, "ui": true, "configPath": ["xpack", "ingestManager"], - "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], - "optionalPlugins": ["security", "features", "cloud", "usageCollection", "home"], + "requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "home"], + "optionalPlugins": ["security", "features", "cloud", "usageCollection" ], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index fde4e93f8e39f..bab4140517dd5 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -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,20 @@ 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: 'Manage ingest', + }), + description: i18n.translate('xpack.ingestManager.featureCatalogueTitle', { + defaultMessage: 'Management for Elastic Agents and integrations.', + }), + icon: 'logstashInput', + showOnHomePage: true, + path: BASE_PATH, + category: FeatureCatalogueCategory.ADMIN, // TODO: is the correct category for this plugin? + }); } return {}; diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 69af475886bb9..a73ac8307f26a 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -176,10 +176,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: [], @@ -189,6 +191,7 @@ export class IngestManagerPlugin read: { api: [`${PLUGIN_ID}-read`], app: [PLUGIN_ID, 'kibana'], + catalogue: ['ingestManager'], savedObject: { all: [], read: allSavedObjectTypes, diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index f2177b0a3572f..907b4ddc6824f 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -78,7 +78,6 @@ export function getPluginPrivileges() { management: { insightsAndAlerting: ['jobsListLink'], }, - catalogue: [PLUGIN_ID], savedObject: { all: [], read: ['index-pattern', 'dashboard', 'search', 'visualization'], @@ -90,11 +89,13 @@ export function getPluginPrivileges() { ...privilege, api: allMlCapabilitiesKeys.map((k) => `ml:${k}`), ui: allMlCapabilitiesKeys, + catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`], }, user: { ...privilege, api: userMlCapabilitiesKeys.map((k) => `ml:${k}`), ui: userMlCapabilitiesKeys, + catalogue: [PLUGIN_ID], }, }; } diff --git a/x-pack/plugins/ml/public/register_feature.ts b/x-pack/plugins/ml/public/register_feature.ts index ca60de612c3d5..179f8ed3b0769 100644 --- a/x-pack/plugins/ml/public/register_feature.ts +++ b/x-pack/plugins/ml/public/register_feature.ts @@ -12,6 +12,9 @@ import { import { PLUGIN_ID } from '../common/constants/app'; export const registerFeature = (home: HomePublicPluginSetup) => { + // TODO: Can this be removed now with homepage redesign work, or does this constitute a breaking change? This is no longer necessary for the home plugin + // if file data visualizer can be registered as its own feature + // register ML for the kibana home screen. // so the file data visualizer appears to allow people to import data home.environment.update({ ml: true }); @@ -31,4 +34,20 @@ export const registerFeature = (home: HomePublicPluginSetup) => { showOnHomePage: true, category: FeatureCatalogueCategory.DATA, }); + + // TODO: is it okay to register this as a separate feature in the feature catalogue? + // register data visualizer so it appears on the Kibana home page + 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: 'importAction', + path: '/app/ml#/filedatavisualizer', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); }; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 812db744d1bda..fb1abc868e965 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -86,7 +86,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'], }, diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 2a04a35830a47..7b252c4643879 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -6,6 +6,9 @@ "xpack", "observability" ], + "requiredPlugins": [ + "home" + ], "optionalPlugins": [ "licensing" ], diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 335ce897dce7b..a0a7058b41e9b 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { i18n } from '@kbn/i18n'; import { AppMountParameters, CoreSetup, @@ -11,6 +13,10 @@ import { PluginInitializerContext, CoreStart, } from '../../../../src/core/public'; +import { + HomePublicPluginSetup, + FeatureCatalogueCategory, +} from '../../../../src/plugins/home/public'; import { registerDataHandler } from './data_handler'; import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; @@ -18,12 +24,16 @@ export interface ObservabilityPluginSetup { dashboard: { register: typeof registerDataHandler }; } +interface SetupPlugins { + home: HomePublicPluginSetup; +} + export type ObservabilityPluginStart = void; export class Plugin implements PluginClass<ObservabilityPluginSetup, ObservabilityPluginStart> { constructor(context: PluginInitializerContext) {} - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: SetupPlugins) { core.application.register({ id: 'observability-overview', title: 'Overview', @@ -41,6 +51,22 @@ export class Plugin implements PluginClass<ObservabilityPluginSetup, Observabili }, }); + if (plugins.home) { + plugins.home.featureCatalogue.register({ + id: 'observability', + title: i18n.translate('xpack.observability.featureCatalogueTitle', { + defaultMessage: 'Observability', + }), + description: i18n.translate('xpack.observability.featureCatalogueDescription', { + defaultMessage: 'Centralize & monitor', + }), + icon: 'logoObservability', + path: '/app/observability', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } + return { dashboard: { register: registerDataHandler }, }; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f934d90c740a5..92c22a316af5a 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -7,7 +7,7 @@ export const APP_ID = 'securitySolution'; export const SERVER_APP_ID = 'siem'; export const APP_NAME = 'Security'; -export const APP_ICON = 'securityAnalyticsApp'; +export const APP_ICON = 'logoSecurity'; export const APP_PATH = `/app/security`; export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 98ea2efe8721e..05964776b31e5 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -69,19 +69,21 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S public setup(core: CoreSetup<StartPlugins, PluginStart>, plugins: SetupPlugins) { initTelemetry(plugins.usageCollection, APP_ID); - plugins.home.featureCatalogue.register({ - id: APP_ID, - title: i18n.translate('xpack.securitySolution.featureCatalogue.title', { - defaultMessage: 'Security', - }), - description: i18n.translate('xpack.securitySolution.featureCatalogue.description', { - defaultMessage: 'Explore security metrics and logs for events and alerts', - }), - icon: APP_ICON, - path: APP_OVERVIEW_PATH, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, - }); + if (plugins.home) { + plugins.home.featureCatalogue.register({ + id: APP_ID, + title: i18n.translate('xpack.securitySolution.featureCatalogue.title', { + defaultMessage: 'Security', + }), + description: i18n.translate('xpack.securitySolution.featureCatalogue.description', { + defaultMessage: 'Protect & prevent', + }), + icon: APP_ICON, + path: APP_OVERVIEW_PATH, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } plugins.triggers_actions_ui.actionTypeRegistry.register(jiraActionType()); plugins.triggers_actions_ui.actionTypeRegistry.register(resilientActionType()); diff --git a/x-pack/plugins/snapshot_restore/public/plugin.ts b/x-pack/plugins/snapshot_restore/public/plugin.ts index b864e70708652..e1b04730bfa64 100644 --- a/x-pack/plugins/snapshot_restore/public/plugin.ts +++ b/x-pack/plugins/snapshot_restore/public/plugin.ts @@ -8,6 +8,10 @@ import { CoreSetup, PluginInitializerContext } from 'src/core/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { + HomePublicPluginSetup, + FeatureCatalogueCategory, +} from '../../../../src/plugins/home/public'; import { PLUGIN } from '../common/constants'; import { ClientConfigType } from './types'; @@ -20,6 +24,7 @@ import { UIM_APP_NAME } from './application/constants'; interface PluginsDependencies { usageCollection: UsageCollectionSetup; management: ManagementSetup; + home?: HomePublicPluginSetup; } export class SnapshotRestoreUIPlugin { @@ -33,7 +38,7 @@ export class SnapshotRestoreUIPlugin { public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): void { const config = this.initializerContext.config.get<ClientConfigType>(); const { http } = coreSetup; - const { management, usageCollection } = plugins; + const { home, management, usageCollection } = plugins; // Initialize services this.uiMetricService.setup(usageCollection); @@ -54,6 +59,23 @@ export class SnapshotRestoreUIPlugin { return await mountManagementSection(coreSetup, services, config, params); }, }); + + if (home) { + home.featureCatalogue.register({ + id: PLUGIN.id, + title: i18n.translate('xpack.snapshotRestore.featureCatalogueTitle', { + defaultMessage: 'Store & recover backups', + }), + description: i18n.translate('xpack.snapshotRestore.featureCatalogueDescription', { + defaultMessage: + 'Use repositories to snapshot and restore Elasticsearch indices and clusters.', + }), + icon: 'storage', + path: '/app/management/data/snapshot_restore', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + } } public start() {}