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 @@
+
\ 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 @@
+
\ 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(