diff --git a/src/plugins/home/common/instruction_variant.ts b/src/plugins/home/common/instruction_variant.ts index 310ee23460a08..f27b2c97bdc1e 100644 --- a/src/plugins/home/common/instruction_variant.ts +++ b/src/plugins/home/common/instruction_variant.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; + export const INSTRUCTION_VARIANT = { ESC: 'esc', OSX: 'osx', @@ -24,6 +26,7 @@ export const INSTRUCTION_VARIANT = { DOTNET: 'dotnet', LINUX: 'linux', PHP: 'php', + FLEET: 'fleet', }; const DISPLAY_MAP = { @@ -44,6 +47,9 @@ const DISPLAY_MAP = { [INSTRUCTION_VARIANT.DOTNET]: '.NET', [INSTRUCTION_VARIANT.LINUX]: 'Linux', [INSTRUCTION_VARIANT.PHP]: 'PHP', + [INSTRUCTION_VARIANT.FLEET]: i18n.translate('home.tutorial.instruction_variant.fleet', { + defaultMessage: 'Elastic APM (beta) in Fleet', + }), }; /** diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 9ab720b47ab92..18f3089c14d11 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { ScopedHistory, CoreStart } from 'kibana/public'; -import { KibanaContextProvider } from '../../../kibana_react/public'; +import { KibanaContextProvider, RedirectAppLinks } from '../../../kibana_react/public'; // @ts-ignore import { HomeApp } from './components/home_app'; import { getServices } from './kibana_services'; @@ -44,9 +44,11 @@ export const renderApp = async ( }); render( - - - , + + + + + , element ); diff --git a/src/plugins/home/public/application/components/tutorial/instruction.js b/src/plugins/home/public/application/components/tutorial/instruction.js index 42c22b057b1e2..373f8c318a504 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,11 +17,23 @@ import { EuiSpacer, EuiCopy, EuiButton, + EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -export function Instruction({ commands, paramValues, textPost, textPre, replaceTemplateStrings }) { +import { getServices } from '../../kibana_services'; + +export function Instruction({ + commands, + paramValues, + textPost, + textPre, + replaceTemplateStrings, + customComponentName, +}) { + const { tutorialService, http, uiSettings, getBasePath } = getServices(); + let pre; if (textPre) { pre = ; @@ -36,6 +48,13 @@ export function Instruction({ commands, paramValues, textPost, textPre, replaceT ); } + const customComponent = tutorialService.getCustomComponent(customComponentName); + //Memoize the custom component so it wont rerender everytime + const LazyCustomComponent = useMemo(() => { + if (customComponent) { + return React.lazy(() => customComponent()); + } + }, [customComponent]); let copyButton; let commandBlock; @@ -79,6 +98,16 @@ export function Instruction({ commands, paramValues, textPost, textPre, replaceT {post} + {LazyCustomComponent && ( + }> + + + )} + ); @@ -90,4 +119,5 @@ Instruction.propTypes = { textPost: PropTypes.string, textPre: PropTypes.string, replaceTemplateStrings: PropTypes.func.isRequired, + customComponentName: PropTypes.string, }; diff --git a/src/plugins/home/public/application/components/tutorial/instruction_set.js b/src/plugins/home/public/application/components/tutorial/instruction_set.js index 8009f3f9657f5..4476929a3f07f 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction_set.js +++ b/src/plugins/home/public/application/components/tutorial/instruction_set.js @@ -186,6 +186,7 @@ class InstructionSetUi extends React.Component { textPre={instruction.textPre} textPost={instruction.textPost} replaceTemplateStrings={this.props.replaceTemplateStrings} + customComponentName={instruction.customComponentName} /> ); return { @@ -282,6 +283,7 @@ const statusCheckConfigShape = PropTypes.shape({ title: PropTypes.string, text: PropTypes.string, btnLabel: PropTypes.string, + customStatusCheck: PropTypes.string, }); InstructionSetUi.propTypes = { diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 539b251bceef1..a7b2f76a1a948 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -67,7 +67,6 @@ class TutorialUi extends React.Component { async componentDidMount() { const tutorial = await this.props.getTutorial(this.props.tutorialId); - if (!this._isMounted) { return; } @@ -172,15 +171,39 @@ class TutorialUi extends React.Component { const instructionSet = this.getInstructionSets()[instructionSetIndex]; const esHitsCheckConfig = _.get(instructionSet, `statusCheck.esHitsCheck`); - if (esHitsCheckConfig) { - const statusCheckState = await this.fetchEsHitsStatus(esHitsCheckConfig); + //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( + this.state.tutorial.customStatusCheckName + ); - this.setState((prevState) => ({ - statusCheckStates: { - ...prevState.statusCheckStates, - [instructionSetIndex]: statusCheckState, - }, - })); + const [esHitsStatusCheck, customStatusCheck] = await Promise.all([ + ...(esHitsCheckConfig ? [this.fetchEsHitsStatus(esHitsCheckConfig)] : []), + ...(customStatusCheckCallback + ? [this.fetchCustomStatusCheck(customStatusCheckCallback)] + : []), + ]); + + const nextStatusCheckState = + esHitsStatusCheck === StatusCheckStates.HAS_DATA || + customStatusCheck === StatusCheckStates.HAS_DATA + ? StatusCheckStates.HAS_DATA + : StatusCheckStates.NO_DATA; + + this.setState((prevState) => ({ + statusCheckStates: { + ...prevState.statusCheckStates, + [instructionSetIndex]: nextStatusCheckState, + }, + })); + }; + + fetchCustomStatusCheck = async (customStatusCheckCallback) => { + try { + const response = await customStatusCheckCallback(); + return response ? StatusCheckStates.HAS_DATA : StatusCheckStates.NO_DATA; + } catch (e) { + return StatusCheckStates.ERROR; } }; diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.test.js b/src/plugins/home/public/application/components/tutorial/tutorial.test.js index 490ecfd8edd78..e9c0b49451e23 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.test.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.test.js @@ -13,12 +13,23 @@ import { Tutorial } from './tutorial'; jest.mock('../../kibana_services', () => ({ getServices: () => ({ + http: { + post: jest.fn().mockImplementation(async () => ({ count: 1 })), + }, getBasePath: jest.fn(() => 'path'), chrome: { setBreadcrumbs: () => {}, }, tutorialService: { getModuleNotices: () => [], + getCustomComponent: jest.fn(), + getCustomStatusCheck: (name) => { + const customStatusCheckMock = { + custom_status_check_has_data: async () => true, + custom_status_check_no_data: async () => false, + }; + return customStatusCheckMock[name]; + }, }, }), })); @@ -54,6 +65,7 @@ const tutorial = { elasticCloud: buildInstructionSet('elasticCloud'), onPrem: buildInstructionSet('onPrem'), onPremElasticCloud: buildInstructionSet('onPremElasticCloud'), + customStatusCheckName: 'custom_status_check_has_data', }; const loadTutorialPromise = Promise.resolve(tutorial); const getTutorial = () => { @@ -143,3 +155,104 @@ test('should render ELASTIC_CLOUD instructions when isCloudEnabled is true', asy component.update(); expect(component).toMatchSnapshot(); // eslint-disable-line }); + +describe('custom status check', () => { + test('should return has_data when custom status check callback is set and returns true', async () => { + const component = mountWithIntl( + {}} + /> + ); + await loadTutorialPromise; + component.update(); + await component.instance().checkInstructionSetStatus(0); + expect(component.state('statusCheckStates')[0]).toEqual('has_data'); + }); + test('should return no_data when custom status check callback is set and returns false', async () => { + const tutorialWithCustomStatusCheckNoData = { + ...tutorial, + customStatusCheckName: 'custom_status_check_no_data', + }; + const component = mountWithIntl( + tutorialWithCustomStatusCheckNoData} + replaceTemplateStrings={replaceTemplateStrings} + tutorialId={'my_testing_tutorial'} + bulkCreate={() => {}} + /> + ); + await loadTutorialPromise; + component.update(); + await component.instance().checkInstructionSetStatus(0); + expect(component.state('statusCheckStates')[0]).toEqual('NO_DATA'); + }); + + test('should return no_data when custom status check callback is not defined', async () => { + const tutorialWithoutCustomStatusCheck = { + ...tutorial, + customStatusCheckName: undefined, + }; + const component = mountWithIntl( + tutorialWithoutCustomStatusCheck} + replaceTemplateStrings={replaceTemplateStrings} + tutorialId={'my_testing_tutorial'} + bulkCreate={() => {}} + /> + ); + await loadTutorialPromise; + component.update(); + await component.instance().checkInstructionSetStatus(0); + expect(component.state('statusCheckStates')[0]).toEqual('NO_DATA'); + }); + + test('should return has_data if esHits or customStatusCheck returns true', async () => { + const { instructionSets } = tutorial.elasticCloud; + const tutorialWithStatusCheckAndCustomStatusCheck = { + ...tutorial, + customStatusCheckName: undefined, + elasticCloud: { + instructionSets: [ + { + ...instructionSets[0], + statusCheck: { + title: 'check status', + text: 'check status', + esHitsCheck: { + index: 'foo', + query: { + bool: { + filter: [{ term: { 'processor.event': 'onboarding' } }], + }, + }, + }, + }, + }, + ], + }, + }; + const component = mountWithIntl( + tutorialWithStatusCheckAndCustomStatusCheck} + replaceTemplateStrings={replaceTemplateStrings} + tutorialId={'my_testing_tutorial'} + bulkCreate={() => {}} + /> + ); + await loadTutorialPromise; + component.update(); + await component.instance().checkInstructionSetStatus(0); + expect(component.state('statusCheckStates')[0]).toEqual('has_data'); + }); +}); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts index ac48168a360d4..0c109d61912ca 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts @@ -15,6 +15,8 @@ const createSetupMock = (): jest.Mocked => { registerDirectoryNotice: jest.fn(), registerDirectoryHeaderLink: jest.fn(), registerModuleNotice: jest.fn(), + registerCustomStatusCheck: jest.fn(), + registerCustomComponent: jest.fn(), }; return setup; }; @@ -26,6 +28,8 @@ const createMock = (): jest.Mocked> => { getDirectoryNotices: jest.fn(() => []), getDirectoryHeaderLinks: jest.fn(() => []), getModuleNotices: jest.fn(() => []), + getCustomStatusCheck: jest.fn(), + getCustomComponent: jest.fn(), }; service.setup.mockImplementation(createSetupMock); return service; diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx index 69d24b66ec6bf..a88cf526e3716 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx +++ b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx @@ -138,4 +138,44 @@ describe('TutorialService', () => { expect(service.getModuleNotices()).toEqual(notices); }); }); + + describe('custom status check', () => { + test('returns undefined when name is customStatusCheckName is empty', () => { + const service = new TutorialService(); + expect(service.getCustomStatusCheck('')).toBeUndefined(); + }); + test('returns undefined when custom status check was not registered', () => { + const service = new TutorialService(); + expect(service.getCustomStatusCheck('foo')).toBeUndefined(); + }); + test('returns custom status check', () => { + const service = new TutorialService(); + const callback = jest.fn(); + service.setup().registerCustomStatusCheck('foo', callback); + const customStatusCheckCallback = service.getCustomStatusCheck('foo'); + expect(customStatusCheckCallback).toBeDefined(); + customStatusCheckCallback(); + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('custom component', () => { + test('returns undefined when name is customComponentName is empty', () => { + const service = new TutorialService(); + expect(service.getCustomComponent('')).toBeUndefined(); + }); + test('returns undefined when custom component was not registered', () => { + const service = new TutorialService(); + expect(service.getCustomComponent('foo')).toBeUndefined(); + }); + test('returns custom component', async () => { + const service = new TutorialService(); + const customComponent =
foo
; + service.setup().registerCustomComponent('foo', async () => customComponent); + const customStatusCheckCallback = service.getCustomComponent('foo'); + expect(customStatusCheckCallback).toBeDefined(); + const result = await customStatusCheckCallback(); + expect(result).toEqual(customComponent); + }); + }); }); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts index 8ba766d34da53..839b0702a499e 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts @@ -22,6 +22,9 @@ export type TutorialModuleNoticeComponent = React.FC<{ moduleName: string; }>; +type CustomStatusCheckCallback = () => Promise; +type CustomComponent = () => Promise; + export class TutorialService { private tutorialVariables: TutorialVariables = {}; private tutorialDirectoryNotices: { [key: string]: TutorialDirectoryNoticeComponent } = {}; @@ -29,6 +32,8 @@ export class TutorialService { [key: string]: TutorialDirectoryHeaderLinkComponent; } = {}; private tutorialModuleNotices: { [key: string]: TutorialModuleNoticeComponent } = {}; + private customStatusCheck: Record = {}; + private customComponent: Record = {}; public setup() { return { @@ -74,6 +79,14 @@ export class TutorialService { } this.tutorialModuleNotices[id] = component; }, + + registerCustomStatusCheck: (name: string, fnCallback: CustomStatusCheckCallback) => { + this.customStatusCheck[name] = fnCallback; + }, + + registerCustomComponent: (name: string, component: CustomComponent) => { + this.customComponent[name] = component; + }, }; } @@ -92,6 +105,14 @@ export class TutorialService { public getModuleNotices() { return Object.values(this.tutorialModuleNotices); } + + public getCustomStatusCheck(customStatusCheckName: string) { + return this.customStatusCheck[customStatusCheckName]; + } + + public getCustomComponent(customComponentName: string) { + return this.customComponent[customComponentName]; + } } export type TutorialServiceSetup = ReturnType; diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts index 840a5944a1343..9523766596fed 100644 --- a/src/plugins/home/server/index.ts +++ b/src/plugins/home/server/index.ts @@ -27,4 +27,9 @@ export const plugin = (initContext: PluginInitializerContext) => new HomeServerP export { INSTRUCTION_VARIANT } from '../common/instruction_variant'; export { TutorialsCategory } from './services/tutorials'; -export type { ArtifactsSchema } from './services/tutorials'; +export type { + ArtifactsSchema, + TutorialSchema, + InstructionSetSchema, + InstructionsSchema, +} from './services/tutorials'; 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 5efbe067f6ece..76b045173a876 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts @@ -56,6 +56,7 @@ const instructionSchema = schema.object({ textPre: schema.maybe(schema.string()), commands: schema.maybe(schema.arrayOf(schema.string())), textPost: schema.maybe(schema.string()), + customComponentName: schema.maybe(schema.string()), }); export type Instruction = TypeOf; @@ -100,7 +101,7 @@ const instructionsSchema = schema.object({ instructionSets: schema.arrayOf(instructionSetSchema), params: schema.maybe(schema.arrayOf(paramSchema)), }); -export type InstructionsSchema = TypeOf; +export type InstructionsSchema = TypeOf; const tutorialIdRegExp = /^[a-zA-Z0-9-]+$/; export const tutorialSchema = schema.object({ @@ -152,6 +153,7 @@ export const tutorialSchema = schema.object({ // saved objects used by data module. savedObjects: schema.maybe(schema.arrayOf(schema.any())), savedObjectsInstallMsg: schema.maybe(schema.string()), + customStatusCheckName: schema.maybe(schema.string()), }); export type TutorialSchema = TypeOf; diff --git a/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg b/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg new file mode 100644 index 0000000000000..b1f86be19a080 --- /dev/null +++ b/x-pack/plugins/apm/public/assets/illustration_integrations_darkmode.svg @@ -0,0 +1 @@ +Kibana-integrations-darkmode \ No newline at end of file diff --git a/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg b/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg new file mode 100644 index 0000000000000..0cddcb0af6909 --- /dev/null +++ b/x-pack/plugins/apm/public/assets/illustration_integrations_lightmode.svg @@ -0,0 +1 @@ +Kibana-integrations-lightmode \ No newline at end of file diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 77e7f2834b080..012856ca9213c 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'; @@ -140,16 +139,42 @@ export class ApmPlugin implements Plugin { ); const getApmDataHelper = async () => { - const { - fetchObservabilityOverviewPageData, - getHasData, - createCallApmApi, - } = await import('./services/rest/apm_observability_overview_fetchers'); + const { fetchObservabilityOverviewPageData, getHasData } = await import( + './services/rest/apm_observability_overview_fetchers' + ); + const { hasFleetApmIntegrations } = await import( + './tutorial/tutorial_apm_fleet_check' + ); + + const { createCallApmApi } = await import( + './services/rest/createCallApmApi' + ); + // have to do this here as well in case app isn't mounted yet createCallApmApi(core); - return { fetchObservabilityOverviewPageData, getHasData }; + return { + fetchObservabilityOverviewPageData, + getHasData, + hasFleetApmIntegrations, + }; }; + + // Registers a status check callback for the tutorial to call and verify if the APM integration is installed on fleet. + pluginSetupDeps.home?.tutorials.registerCustomStatusCheck( + 'apm_fleet_server_status_check', + async () => { + const { hasFleetApmIntegrations } = await getApmDataHelper(); + return hasFleetApmIntegrations(); + } + ); + + // Registers custom component that is going to be render on fleet section + pluginSetupDeps.home?.tutorials.registerCustomComponent( + 'TutorialFleetInstructions', + () => import('./tutorial/tutorial_fleet_instructions') + ); + plugins.observability.dashboard.register({ appName: 'apm', hasData: async () => { @@ -163,11 +188,12 @@ export class ApmPlugin implements Plugin { }); const getUxDataHelper = async () => { - const { - fetchUxOverviewDate, - hasRumData, - createCallApmApi, - } = await import('./components/app/RumDashboard/ux_overview_fetchers'); + const { fetchUxOverviewDate, hasRumData } = await import( + './components/app/RumDashboard/ux_overview_fetchers' + ); + const { createCallApmApi } = await import( + './services/rest/createCallApmApi' + ); // have to do this here as well in case app isn't mounted yet createCallApmApi(core); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index ef61e25af4fc2..1b95c88a5fdc5 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -11,8 +11,6 @@ import { } from '../../../../observability/public'; import { callApmApi } from './createCallApmApi'; -export { createCallApmApi } from './createCallApmApi'; - export const fetchObservabilityOverviewPageData = async ({ absoluteTime, relativeTime, diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts b/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts new file mode 100644 index 0000000000000..8db8614d606a9 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/tutorial_apm_fleet_check.ts @@ -0,0 +1,20 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { callApmApi } from '../services/rest/createCallApmApi'; + +export async function hasFleetApmIntegrations() { + try { + const { hasData = false } = await callApmApi({ + endpoint: 'GET /api/apm/fleet/has_data', + signal: null, + }); + return hasData; + } catch (e) { + console.error('Something went wrong while fetching apm fleet data', e); + return false; + } +} diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx new file mode 100644 index 0000000000000..8a81b7a994e76 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx @@ -0,0 +1,122 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +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 { APIReturnType } from '../../services/rest/createCallApmApi'; + +interface Props { + http: HttpStart; + basePath: string; + isDarkTheme: boolean; +} + +const CentralizedContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +type APIResponseType = APIReturnType<'GET /api/apm/fleet/has_data'>; + +function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) { + const [data, setData] = useState(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + async function fetchData() { + setIsLoading(true); + try { + const response = await http.get('/api/apm/fleet/has_data'); + setData(response as APIResponseType); + } catch (e) { + console.error('Error while fetching fleet details.', e); + } + setIsLoading(false); + } + fetchData(); + }, [http]); + + if (isLoading) { + return ( + + + + ); + } + + // When APM integration is enable in Fleet + if (data?.hasData) { + return ( + + {i18n.translate( + 'xpack.apm.tutorial.apmServer.fleet.manageApmIntegration.button', + { + defaultMessage: 'Manage APM integration in Fleet', + } + )} + + ); + } + // When APM integration is not installed in Fleet or for some reason the API didn't work out + return ( + + + + + {i18n.translate( + 'xpack.apm.tutorial.apmServer.fleet.apmIntegration.button', + { + defaultMessage: 'APM integration', + } + )} + + } + /> + + + + + + + ); +} +// eslint-disable-next-line import/no-default-export +export default TutorialFleetInstructions; diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts new file mode 100644 index 0000000000000..74ca8dc368dad --- /dev/null +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -0,0 +1,36 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; +import { getApmPackgePolicies } from '../lib/fleet/get_apm_package_policies'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; + +const hasFleetDataRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/fleet/has_data', + options: { tags: [] }, + handler: async ({ core, plugins }) => { + const fleetPluginStart = await plugins.fleet?.start(); + if (!fleetPluginStart) { + throw Boom.internal( + i18n.translate('xpack.apm.fleet_has_data.fleetRequired', { + defaultMessage: `Fleet plugin is required`, + }) + ); + } + const packagePolicies = await getApmPackgePolicies({ + core, + fleetPluginStart, + }); + return { hasData: packagePolicies.total > 0 }; + }, +}); + +export const ApmFleetRouteRepository = createApmServerRouteRepository().add( + hasFleetDataRoute +); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index f1c08444d2e1e..fa2f80f073958 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -30,6 +30,7 @@ import { sourceMapsRouteRepository } from './source_maps'; import { traceRouteRepository } from './traces'; import { transactionRouteRepository } from './transactions'; import { APMRouteHandlerResources } from './typings'; +import { ApmFleetRouteRepository } from './fleet'; const getTypedGlobalApmServerRouteRepository = () => { const repository = createApmServerRouteRepository() @@ -50,7 +51,8 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(anomalyDetectionRouteRepository) .merge(apmIndicesRouteRepository) .merge(customLinkRouteRepository) - .merge(sourceMapsRouteRepository); + .merge(sourceMapsRouteRepository) + .merge(ApmFleetRouteRepository); return repository; }; diff --git a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts index c6afd6a592fff..55adc756f31af 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts @@ -6,7 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../../../../../../src/plugins/home/server'; +import { + INSTRUCTION_VARIANT, + TutorialSchema, + InstructionSetSchema, +} from '../../../../../../src/plugins/home/server'; import { createNodeAgentInstructions, @@ -22,7 +26,9 @@ import { } from '../instructions/apm_agent_instructions'; import { CloudSetup } from '../../../../cloud/server'; -export function createElasticCloudInstructions(cloudSetup?: CloudSetup) { +export function createElasticCloudInstructions( + cloudSetup?: CloudSetup +): TutorialSchema['elasticCloud'] { const apmServerUrl = cloudSetup?.apm.url; const instructionSets = []; @@ -37,7 +43,9 @@ export function createElasticCloudInstructions(cloudSetup?: CloudSetup) { }; } -function getApmServerInstructionSet(cloudSetup?: CloudSetup) { +function getApmServerInstructionSet( + cloudSetup?: CloudSetup +): InstructionSetSchema { const cloudId = cloudSetup?.cloudId; return { title: i18n.translate('xpack.apm.tutorial.apmServer.title', { @@ -61,7 +69,9 @@ function getApmServerInstructionSet(cloudSetup?: CloudSetup) { }; } -function getApmAgentInstructionSet(cloudSetup?: CloudSetup) { +function getApmAgentInstructionSet( + cloudSetup?: CloudSetup +): InstructionSetSchema { const apmServerUrl = cloudSetup?.apm.url; const secretToken = cloudSetup?.apm.secretToken; diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index a0e96f563381c..882d45c4c21db 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -6,28 +6,31 @@ */ import { i18n } from '@kbn/i18n'; -import { INSTRUCTION_VARIANT } from '../../../../../../src/plugins/home/server'; import { - createWindowsServerInstructions, - createEditConfig, - createStartServerUnixSysv, - createStartServerUnix, - createDownloadServerRpm, - createDownloadServerDeb, - createDownloadServerOsx, -} from '../instructions/apm_server_instructions'; + INSTRUCTION_VARIANT, + InstructionsSchema, +} from '../../../../../../src/plugins/home/server'; import { - createNodeAgentInstructions, createDjangoAgentInstructions, + createDotNetAgentInstructions, createFlaskAgentInstructions, - createRailsAgentInstructions, - createRackAgentInstructions, - createJsAgentInstructions, createGoAgentInstructions, createJavaAgentInstructions, - createDotNetAgentInstructions, + createJsAgentInstructions, + createNodeAgentInstructions, createPhpAgentInstructions, + createRackAgentInstructions, + createRailsAgentInstructions, } from '../instructions/apm_agent_instructions'; +import { + createDownloadServerDeb, + createDownloadServerOsx, + createDownloadServerRpm, + createEditConfig, + createStartServerUnix, + createStartServerUnixSysv, + createWindowsServerInstructions, +} from '../instructions/apm_server_instructions'; export function onPremInstructions({ errorIndices, @@ -41,7 +44,7 @@ export function onPremInstructions({ metricsIndices: string; sourcemapIndices: string; onboardingIndices: string; -}) { +}): InstructionsSchema { const EDIT_CONFIG = createEditConfig(); const START_SERVER_UNIX = createStartServerUnix(); const START_SERVER_UNIX_SYSV = createStartServerUnixSysv(); @@ -66,6 +69,12 @@ export function onPremInstructions({ iconType: 'alert', }, instructionVariants: [ + { + id: INSTRUCTION_VARIANT.FLEET, + instructions: [ + { customComponentName: 'TutorialFleetInstructions' }, + ], + }, { id: INSTRUCTION_VARIANT.OSX, instructions: [ diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index d678677a4b751..9118c30b845d0 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -6,15 +6,16 @@ */ import { i18n } from '@kbn/i18n'; -import { onPremInstructions } from './envs/on_prem'; -import { createElasticCloudInstructions } from './envs/elastic_cloud'; -import apmIndexPattern from './index_pattern.json'; -import { CloudSetup } from '../../../cloud/server'; import { ArtifactsSchema, TutorialsCategory, + TutorialSchema, } from '../../../../../src/plugins/home/server'; +import { CloudSetup } from '../../../cloud/server'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants'; +import { createElasticCloudInstructions } from './envs/elastic_cloud'; +import { onPremInstructions } from './envs/on_prem'; +import apmIndexPattern from './index_pattern.json'; const apmIntro = i18n.translate('xpack.apm.tutorial.introduction', { defaultMessage: @@ -102,6 +103,7 @@ It allows you to monitor the performance of thousands of applications in real ti ), euiIconType: 'apmApp', artifacts, + customStatusCheckName: 'apm_fleet_server_status_check', onPrem: onPremInstructions(indices), elasticCloud: createElasticCloudInstructions(cloud), previewImagePath: '/plugins/apm/assets/apm.png', @@ -113,5 +115,5 @@ It allows you to monitor the performance of thousands of applications in real ti 'An APM index pattern is required for some features in the APM UI.', } ), - }; + } as TutorialSchema; }; diff --git a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts index a25021fac5d00..ba11a996f00df 100644 --- a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts +++ b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts @@ -913,7 +913,10 @@ export const createPhpAgentInstructions = ( 'APM is automatically started when your app boots. Configure the agent either via `php.ini` file:', } ), - commands: `elastic_apm.server_url=http://localhost:8200 + commands: `elastic_apm.server_url="${ + apmServerUrl || 'http://localhost:8200' + }" +elastic.apm.secret_token="${secretToken}" elastic_apm.service_name="My service" `.split('\n'), textPost: i18n.translate(