Skip to content

Commit

Permalink
[APM] Link to Fleet APM Server Configuration when managed by Elastic …
Browse files Browse the repository at this point in the history
…Agent w/Fleet (#100816) (#103302)

* 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 <[email protected]>

Co-authored-by: Cauê Marcondes <[email protected]>
  • Loading branch information
kibanamachine and cauemarcondes authored Jun 24, 2021
1 parent fff2251 commit 669839c
Show file tree
Hide file tree
Showing 23 changed files with 535 additions and 57 deletions.
6 changes: 6 additions & 0 deletions src/plugins/home/common/instruction_variant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* Side Public License, v 1.
*/

import { i18n } from '@kbn/i18n';

export const INSTRUCTION_VARIANT = {
ESC: 'esc',
OSX: 'osx',
Expand All @@ -24,6 +26,7 @@ export const INSTRUCTION_VARIANT = {
DOTNET: 'dotnet',
LINUX: 'linux',
PHP: 'php',
FLEET: 'fleet',
};

const DISPLAY_MAP = {
Expand All @@ -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',
}),
};

/**
Expand Down
10 changes: 6 additions & 4 deletions src/plugins/home/public/application/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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)} />;
Expand All @@ -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;
Expand Down Expand Up @@ -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>
);
Expand All @@ -90,4 +119,5 @@ Instruction.propTypes = {
textPost: PropTypes.string,
textPre: PropTypes.string,
replaceTemplateStrings: PropTypes.func.isRequired,
customComponentName: PropTypes.string,
};
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ class InstructionSetUi extends React.Component {
textPre={instruction.textPre}
textPost={instruction.textPost}
replaceTemplateStrings={this.props.replaceTemplateStrings}
customComponentName={instruction.customComponentName}
/>
);
return {
Expand Down Expand Up @@ -282,6 +283,7 @@ const statusCheckConfigShape = PropTypes.shape({
title: PropTypes.string,
text: PropTypes.string,
btnLabel: PropTypes.string,
customStatusCheck: PropTypes.string,
});

InstructionSetUi.propTypes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ class TutorialUi extends React.Component {

async componentDidMount() {
const tutorial = await this.props.getTutorial(this.props.tutorialId);

if (!this._isMounted) {
return;
}
Expand Down Expand Up @@ -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;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
},
},
}),
}));
Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Loading

0 comments on commit 669839c

Please sign in to comment.