Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jun 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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