mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
be1c5bbd72
commit
f2937720aa
23 changed files with 535 additions and 57 deletions
|
@ -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',
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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']>;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 |
|
@ -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);
|
||||
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
} from '../../../../observability/public';
|
||||
import { callApmApi } from './createCallApmApi';
|
||||
|
||||
export { createCallApmApi } from './createCallApmApi';
|
||||
|
||||
export const fetchObservabilityOverviewPageData = async ({
|
||||
absoluteTime,
|
||||
relativeTime,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
36
x-pack/plugins/apm/server/routes/fleet.ts
Normal file
36
x-pack/plugins/apm/server/routes/fleet.ts
Normal 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
|
||||
);
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue