diff --git a/src/plugins/home/public/application/components/tutorial/instruction.js b/src/plugins/home/public/application/components/tutorial/instruction.js index 88b28ea1bf1a9..5462263f0877b 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction.js +++ b/src/plugins/home/public/application/components/tutorial/instruction.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { Suspense, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Content } from './content'; @@ -17,20 +17,16 @@ import { EuiSpacer, EuiCopy, EuiButton, + EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { APMFleet } from '../apm_fleet'; +import { getServices } from '../../kibana_services'; + +export function Instruction({ commands, paramValues, textPost, textPre, replaceTemplateStrings }) { + const { tutorialService, http, uiSettings, getBasePath } = getServices(); -export function Instruction({ - commands, - paramValues, - textPost, - textPre, - replaceTemplateStrings, - customComponent, -}) { let pre; if (textPre) { pre = ; @@ -45,10 +41,13 @@ export function Instruction({ ); } - let custom; - if (customComponent === 'apm_fleet') { - custom = ; - } + const customComponent = tutorialService.getCustomComponent(); + //Memoize the custom component so it wont rerender everytime + const LazyCustomComponent = useMemo(() => { + if (customComponent) { + return React.lazy(() => customComponent()); + } + }, [customComponent]); let copyButton; let commandBlock; @@ -92,7 +91,15 @@ export function Instruction({ {post} - {custom} + {LazyCustomComponent && ( + }> + + + )} diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 9ee9a92714a76..9296f72c8d44c 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -67,12 +67,12 @@ class TutorialUi extends React.Component { async componentDidMount() { const tutorial = await this.props.getTutorial(this.props.tutorialId); - if (!this._isMounted) { return; } if (tutorial) { + getServices().tutorialService.setTutorial(tutorial); // eslint-disable-next-line react/no-did-mount-set-state this.setState( { @@ -172,12 +172,15 @@ class TutorialUi extends React.Component { const instructionSet = this.getInstructionSets()[instructionSetIndex]; const esHitsCheckConfig = _.get(instructionSet, `statusCheck.esHitsCheck`); - //Checks if the tutorial registered in the SERVER contains the customStatusCheckName property - const { customStatusCheckName } = this.state.tutorial; + //Checks if a custom status check callback was registered in the CLIENT + //that matches the same name registered in the SERVER (customStatusCheckName) + const customStatusCheckCallback = getServices().tutorialService.getCustomStatusCheck(); const [esHitsStatusCheck, customStatusCheck] = await Promise.all([ ...(esHitsCheckConfig ? [this.fetchEsHitsStatus(esHitsCheckConfig)] : []), - ...(customStatusCheckName ? [this.fetchCustomStatusCheck(customStatusCheckName)] : []), + ...(customStatusCheckCallback + ? [this.fetchCustomStatusCheck(customStatusCheckCallback)] + : []), ]); const nextStatusCheckState = @@ -194,17 +197,10 @@ class TutorialUi extends React.Component { })); }; - fetchCustomStatusCheck = async (customStatusCheckName) => { + fetchCustomStatusCheck = async (customStatusCheckCallback) => { try { - //Checks if a custom status check callback was registered in the CLIENT - //that matches the same name registered in the SERVER (customStatusCheckName) - const customStatusCheckCallback = getServices().tutorialService.getCustomStatusCheck( - customStatusCheckName - ); - if (customStatusCheckCallback) { - const response = await customStatusCheckCallback(); - return response ? StatusCheckStates.HAS_DATA : StatusCheckStates.NO_DATA; - } + const response = await customStatusCheckCallback(); + return response ? StatusCheckStates.HAS_DATA : StatusCheckStates.NO_DATA; } catch (e) { return StatusCheckStates.ERROR; } diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts index b558f01997b59..7ab1d124adb00 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts @@ -23,7 +23,7 @@ export type TutorialModuleNoticeComponent = React.FC<{ }>; type CustomStatusCheckCallback = () => Promise; -type CustomStatusCheck = Record; +type CustomComponent = () => Promise; export class TutorialService { private tutorialVariables: TutorialVariables = {}; @@ -32,7 +32,9 @@ export class TutorialService { [key: string]: TutorialDirectoryHeaderLinkComponent; } = {}; private tutorialModuleNotices: { [key: string]: TutorialModuleNoticeComponent } = {}; - private customStatusCheck: CustomStatusCheck = {}; + private customStatusCheck: Record = {}; + private customComponent: Record = {}; + private tutorial: any; public setup() { return { @@ -82,6 +84,10 @@ export class TutorialService { registerCustomStatusCheck: (name: string, fnCallback: CustomStatusCheckCallback) => { this.customStatusCheck[name] = fnCallback; }, + + registerCustomComponent: (name: string, component: CustomComponent) => { + this.customComponent[name] = component; + }, }; } @@ -101,8 +107,20 @@ export class TutorialService { return Object.values(this.tutorialModuleNotices); } - public getCustomStatusCheck(name: string) { - return this.customStatusCheck[name]; + public getCustomStatusCheck() { + return this.customStatusCheck[this.tutorial?.customComponentName]; + } + + public getCustomComponent() { + return this.customComponent[this.tutorial?.customComponentName]; + } + + public setTutorial(tutorial: any) { + this.tutorial = tutorial; + } + + public getTutorial() { + return this.tutorial; } } diff --git a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts index 375728e175f2c..6ea3c6aefa9ad 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts @@ -154,6 +154,7 @@ export const tutorialSchema = schema.object({ savedObjects: schema.maybe(schema.arrayOf(schema.any())), savedObjectsInstallMsg: schema.maybe(schema.string()), customStatusCheckName: schema.maybe(schema.string()), + customComponentName: schema.maybe(schema.string()), }); export type TutorialSchema = TypeOf; diff --git a/src/plugins/home/public/application/components/apm_fleet/index.tsx b/x-pack/plugins/apm/public/components/shared/tutorial_fleet_instructions/index.tsx similarity index 54% rename from src/plugins/home/public/application/components/apm_fleet/index.tsx rename to x-pack/plugins/apm/public/components/shared/tutorial_fleet_instructions/index.tsx index 1dc1b1e5de1b6..66aa149067acc 100644 --- a/src/plugins/home/public/application/components/apm_fleet/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/tutorial_fleet_instructions/index.tsx @@ -1,23 +1,27 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ -import { - EuiButton, - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiImage, - EuiLoadingSpinner, - EuiPanel, -} from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; +import { EuiCard } from '@elastic/eui'; +import { EuiImage } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { HttpStart } from 'kibana/public'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { getServices } from '../../kibana_services'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; + +interface Props { + http: HttpStart; + basePath: string; + isDarkTheme: boolean; +} const CentralizedContainer = styled.div` display: flex; @@ -25,15 +29,10 @@ const CentralizedContainer = styled.div` align-items: center; `; -interface APIResponse { - hasData: boolean; -} +type APIResponseType = APIReturnType<'GET /api/apm/fleet/has_data'>; -export function APMFleet() { - const { http, getBasePath, uiSettings } = getServices(); - const isDarkTheme = uiSettings.get('theme:darkMode'); - const basePath = getBasePath(); - const [data, setData] = useState(); +function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) { + const [data, setData] = useState(); const [isLoading, setIsLoading] = useState(false); useEffect(() => { @@ -41,9 +40,8 @@ export function APMFleet() { setIsLoading(true); try { const response = await http.get('/api/apm/fleet/has_data'); - setData(response as APIResponse); + setData(response as APIResponseType); } catch (e) { - // eslint-disable-next-line no-console console.error('Error while fetching fleet details.', e); } setIsLoading(false); @@ -58,13 +56,17 @@ export function APMFleet() { ); } + // When APM integration is enable in Fleet if (data?.hasData) { return ( - {i18n.translate('home.apm.tutorial.apmServer.fleet.manageApmIntegration.button', { - defaultMessage: 'Manage APM integration in Fleet', - })} + {i18n.translate( + 'xpack.apm.tutorial.apmServer.fleet.manageApmIntegration.button', + { + defaultMessage: 'Manage APM integration in Fleet', + } + )} ); } @@ -76,22 +78,28 @@ export function APMFleet() { - {i18n.translate('home.apm.tutorial.apmServer.fleet.apmIntegration.button', { - defaultMessage: 'APM integration', - })} + {i18n.translate( + 'xpack.apm.tutorial.apmServer.fleet.apmIntegration.button', + { + defaultMessage: 'APM integration', + } + )} } /> @@ -110,3 +118,5 @@ export function APMFleet() { ); } +// eslint-disable-next-line import/no-default-export +export default TutorialFleetInstructions; diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index fcf34a916e809..6f332bf9ef26b 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { i18n } from '@kbn/i18n'; import { from } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -165,6 +164,13 @@ export class ApmPlugin implements Plugin { return hasFleetApmIntegrations(); } ); + + // Registers custom component that is going to be render on fleet section + pluginSetupDeps.home?.tutorials.registerCustomComponent( + 'TutorialFleetInstructions', + () => import('./components/shared/tutorial_fleet_instructions') + ); + plugins.observability.dashboard.register({ appName: 'apm', hasData: async () => { diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index 9118c30b845d0..b4cca1bee94b0 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -104,6 +104,7 @@ It allows you to monitor the performance of thousands of applications in real ti euiIconType: 'apmApp', artifacts, customStatusCheckName: 'apm_fleet_server_status_check', + customComponentName: 'TutorialFleetInstructions', onPrem: onPremInstructions(indices), elasticCloud: createElasticCloudInstructions(cloud), previewImagePath: '/plugins/apm/assets/apm.png',