[APM] Link to Fleet APM Server Configuration when managed by Elastic Agent w/Fleet (#100816)

* Register tutorial on APM plugin

* using files from apm

* removing tutorial from apm_oss

* removing export

* fixing i18n

* adding fleet section

* adding fleet information on APM tutorial

* adding fleet typing

* fixing i18n

* adding fleet information on APM tutorial

* checks apm fleet integration when pushing button

* adding fleet information on APM tutorial

* refactoring

* registering status check callback

* addin custom component registration function

* fixing TS issue

* addressing PR comments

* fixing tests

* adding i18n

* fixing issues

* adding unit test

* adding unit test

* addressing PR comments

* fixing TS issue

* moving tutorial to a common directory

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2021-06-24 10:53:15 -04:00 committed by GitHub
parent be1c5bbd72
commit f2937720aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 535 additions and 57 deletions

View file

@ -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',
}),
};
/**

View file

@ -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(
<KibanaContextProvider services={{ ...coreStart }}>
<HomeApp directories={directories} solutions={solutions} />
</KibanaContextProvider>,
<RedirectAppLinks application={coreStart.application}>
<KibanaContextProvider services={{ ...coreStart }}>
<HomeApp directories={directories} solutions={solutions} />
</KibanaContextProvider>
</RedirectAppLinks>,
element
);

View file

@ -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 = <Content text={replaceTemplateStrings(textPre)} />;
@ -36,6 +48,13 @@ export function Instruction({ commands, paramValues, textPost, textPre, replaceT
</div>
);
}
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 && (
<Suspense fallback={<EuiLoadingSpinner />}>
<LazyCustomComponent
basePath={getBasePath()}
isDarkTheme={uiSettings.get('theme:darkMode')}
http={http}
/>
</Suspense>
)}
<EuiSpacer />
</div>
);
@ -90,4 +119,5 @@ Instruction.propTypes = {
textPost: PropTypes.string,
textPre: PropTypes.string,
replaceTemplateStrings: PropTypes.func.isRequired,
customComponentName: PropTypes.string,
};

View file

@ -186,6 +186,7 @@ class InstructionSetUi extends React.Component {
textPre={instruction.textPre}
textPost={instruction.textPost}
replaceTemplateStrings={this.props.replaceTemplateStrings}
customComponentName={instruction.customComponentName}
/>
);
return {
@ -298,6 +299,7 @@ const statusCheckConfigShape = PropTypes.shape({
title: PropTypes.string,
text: PropTypes.string,
btnLabel: PropTypes.string,
customStatusCheck: PropTypes.string,
});
InstructionSetUi.propTypes = {

View file

@ -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;
}
};

View file

@ -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(
<Tutorial.WrappedComponent
addBasePath={addBasePath}
isCloudEnabled={true}
getTutorial={getTutorial}
replaceTemplateStrings={replaceTemplateStrings}
tutorialId={'my_testing_tutorial'}
bulkCreate={() => {}}
/>
);
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(
<Tutorial.WrappedComponent
addBasePath={addBasePath}
isCloudEnabled={true}
getTutorial={async () => 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(
<Tutorial.WrappedComponent
addBasePath={addBasePath}
isCloudEnabled={true}
getTutorial={async () => 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(
<Tutorial.WrappedComponent
addBasePath={addBasePath}
isCloudEnabled={true}
getTutorial={async () => 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');
});
});

View file

@ -15,6 +15,8 @@ const createSetupMock = (): jest.Mocked<TutorialServiceSetup> => {
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<PublicMethodsOf<TutorialService>> => {
getDirectoryNotices: jest.fn(() => []),
getDirectoryHeaderLinks: jest.fn(() => []),
getModuleNotices: jest.fn(() => []),
getCustomStatusCheck: jest.fn(),
getCustomComponent: jest.fn(),
};
service.setup.mockImplementation(createSetupMock);
return service;

View file

@ -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 = <div>foo</div>;
service.setup().registerCustomComponent('foo', async () => customComponent);
const customStatusCheckCallback = service.getCustomComponent('foo');
expect(customStatusCheckCallback).toBeDefined();
const result = await customStatusCheckCallback();
expect(result).toEqual(customComponent);
});
});
});

View file

@ -22,6 +22,9 @@ export type TutorialModuleNoticeComponent = React.FC<{
moduleName: string;
}>;
type CustomStatusCheckCallback = () => Promise<boolean>;
type CustomComponent = () => Promise<React.ReactNode>;
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<string, CustomStatusCheckCallback> = {};
private customComponent: Record<string, CustomComponent> = {};
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<TutorialService['setup']>;

View file

@ -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';

View file

@ -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<typeof instructionSchema>;
@ -100,7 +101,7 @@ const instructionsSchema = schema.object({
instructionSets: schema.arrayOf(instructionSetSchema),
params: schema.maybe(schema.arrayOf(paramSchema)),
});
export type InstructionsSchema = TypeOf<typeof instructionSchema>;
export type InstructionsSchema = TypeOf<typeof instructionsSchema>;
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<typeof tutorialSchema>;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 245 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 245 KiB

View file

@ -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<ApmPluginSetup, ApmPluginStart> {
);
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<ApmPluginSetup, ApmPluginStart> {
});
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);

View file

@ -11,8 +11,6 @@ import {
} from '../../../../observability/public';
import { callApmApi } from './createCallApmApi';
export { createCallApmApi } from './createCallApmApi';
export const fetchObservabilityOverviewPageData = async ({
absoluteTime,
relativeTime,

View file

@ -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;
}
}

View file

@ -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<APIResponseType | undefined>();
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 (
<CentralizedContainer>
<EuiLoadingSpinner />
</CentralizedContainer>
);
}
// When APM integration is enable in Fleet
if (data?.hasData) {
return (
<EuiButton iconType="gear" fill href={`${basePath}/app/fleet#/policies`}>
{i18n.translate(
'xpack.apm.tutorial.apmServer.fleet.manageApmIntegration.button',
{
defaultMessage: 'Manage APM integration in Fleet',
}
)}
</EuiButton>
);
}
// When APM integration is not installed in Fleet or for some reason the API didn't work out
return (
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem grow={7}>
<EuiCard
display="plain"
textAlign="left"
title={i18n.translate('xpack.apm.tutorial.apmServer.fleet.title', {
defaultMessage: 'Elastic APM (beta) now available in Fleet!',
})}
description={i18n.translate(
'xpack.apm.tutorial.apmServer.fleet.message',
{
defaultMessage:
'The APM integration installs Elasticsearch templates and Ingest Node pipelines for APM data.',
}
)}
footer={
<EuiButton
iconType="analyzeEvent"
color="secondary"
href={`${basePath}/app/integrations#/detail/apm-0.3.0/overview`}
>
{i18n.translate(
'xpack.apm.tutorial.apmServer.fleet.apmIntegration.button',
{
defaultMessage: 'APM integration',
}
)}
</EuiButton>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiImage
src={`${basePath}/plugins/apm/assets/${
isDarkTheme
? 'illustration_integrations_darkmode.svg'
: 'illustration_integrations_lightmode.svg'
}`}
alt="Illustration"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
// eslint-disable-next-line import/no-default-export
export default TutorialFleetInstructions;

View file

@ -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
);

View file

@ -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;
};

View file

@ -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;

View file

@ -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: [

View file

@ -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;
};

View file

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