Skip to content

Commit

Permalink
[APM] Add anomaly detection API tests + fixes (#73120)
Browse files Browse the repository at this point in the history
Co-authored-by: Nathan L Smith <[email protected]>
  • Loading branch information
sorenlouv and smith authored Jul 30, 2020
1 parent 6fc193c commit aa68e3b
Show file tree
Hide file tree
Showing 25 changed files with 494 additions and 262 deletions.
23 changes: 5 additions & 18 deletions x-pack/plugins/apm/common/anomaly_detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@ export interface ServiceAnomalyStats {
jobId?: string;
}

export const MLErrorMessages: Record<ErrorCode, string> = {
INSUFFICIENT_LICENSE: i18n.translate(
'xpack.apm.anomaly_detection.error.insufficient_license',
export const ML_ERRORS = {
INVALID_LICENSE: i18n.translate(
'xpack.apm.anomaly_detection.error.invalid_license',
{
defaultMessage:
'You must have a platinum license to use Anomaly Detection',
defaultMessage: `To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.`,
}
),
MISSING_READ_PRIVILEGES: i18n.translate(
'xpack.apm.anomaly_detection.error.missing_read_privileges',
{
defaultMessage:
'You must have "read" privileges to Machine Learning in order to view Anomaly Detection jobs',
'You must have "read" privileges to Machine Learning and APM in order to view Anomaly Detection jobs',
}
),
MISSING_WRITE_PRIVILEGES: i18n.translate(
Expand All @@ -47,16 +46,4 @@ export const MLErrorMessages: Record<ErrorCode, string> = {
defaultMessage: 'Machine learning is not available in the selected space',
}
),
UNEXPECTED: i18n.translate('xpack.apm.anomaly_detection.error.unexpected', {
defaultMessage: 'An unexpected error occurred',
}),
};

export enum ErrorCode {
INSUFFICIENT_LICENSE = 'INSUFFICIENT_LICENSE',
MISSING_READ_PRIVILEGES = 'MISSING_READ_PRIVILEGES',
MISSING_WRITE_PRIVILEGES = 'MISSING_WRITE_PRIVILEGES',
ML_NOT_AVAILABLE = 'ML_NOT_AVAILABLE',
ML_NOT_AVAILABLE_IN_SPACE = 'ML_NOT_AVAILABLE_IN_SPACE',
UNEXPECTED = 'UNEXPECTED',
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
import { ML_ERRORS } from '../../../../../common/anomaly_detection';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { createJobs } from './create_jobs';
Expand Down Expand Up @@ -65,7 +65,7 @@ export function AddEnvironments({
<EuiPanel>
<EuiEmptyPrompt
iconType="warning"
body={<>{MLErrorMessages.MISSING_WRITE_PRIVILEGES}</>}
body={<>{ML_ERRORS.MISSING_WRITE_PRIVILEGES}</>}
/>
</EuiPanel>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import { i18n } from '@kbn/i18n';
import { NotificationsStart } from 'kibana/public';
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
import { callApmApi } from '../../../../services/rest/createCallApmApi';

const errorToastTitle = i18n.translate(
Expand All @@ -27,31 +26,19 @@ export async function createJobs({
toasts: NotificationsStart['toasts'];
}) {
try {
const res = await callApmApi({
await callApmApi({
pathname: '/api/apm/settings/anomaly-detection/jobs',
method: 'POST',
params: {
body: { environments },
},
});

// a known error occurred
if (res?.errorCode) {
toasts.addDanger({
title: errorToastTitle,
text: MLErrorMessages[res.errorCode],
});
return false;
}

// job created successfully
toasts.addSuccess({
title: successToastTitle,
text: getSuccessToastMessage(environments),
});
return true;

// an unknown/unexpected error occurred
} catch (error) {
toasts.addDanger({
title: errorToastTitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, { useState } from 'react';
import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui';
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
import { ML_ERRORS } from '../../../../../common/anomaly_detection';
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
import { JobsList } from './jobs_list';
import { AddEnvironments } from './add_environments';
Expand All @@ -25,7 +25,6 @@ export type AnomalyDetectionApiResponse = APIReturnType<
const DEFAULT_VALUE: AnomalyDetectionApiResponse = {
jobs: [],
hasLegacyJobs: false,
errorCode: undefined,
};

export function AnomalyDetection() {
Expand All @@ -49,15 +48,7 @@ export function AnomalyDetection() {
if (!hasValidLicense) {
return (
<EuiPanel>
<LicensePrompt
text={i18n.translate(
'xpack.apm.settings.anomaly_detection.license.text',
{
defaultMessage:
"To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability monitor your services with the aid of machine learning.",
}
)}
/>
<LicensePrompt text={ML_ERRORS.INVALID_LICENSE} />
</EuiPanel>
);
}
Expand All @@ -67,7 +58,7 @@ export function AnomalyDetection() {
<EuiPanel>
<EuiEmptyPrompt
iconType="warning"
body={<>{MLErrorMessages.MISSING_READ_PRIVILEGES}</>}
body={<>{ML_ERRORS.MISSING_READ_PRIVILEGES}</>}
/>
</EuiPanel>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
MLErrorMessages,
ErrorCode,
} from '../../../../../common/anomaly_detection';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
Expand Down Expand Up @@ -66,7 +62,7 @@ interface Props {
onAddEnvironments: () => void;
}
export function JobsList({ data, status, onAddEnvironments }: Props) {
const { jobs, hasLegacyJobs, errorCode } = data;
const { jobs, hasLegacyJobs } = data;

return (
<EuiPanel>
Expand Down Expand Up @@ -115,10 +111,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {
</EuiText>
<EuiSpacer size="l" />
<ManagedTable
noItemsMessage={getNoItemsMessage({
status,
errorCode,
})}
noItemsMessage={getNoItemsMessage({ status })}
columns={columns}
items={jobs}
/>
Expand All @@ -129,25 +122,14 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {
);
}

function getNoItemsMessage({
status,
errorCode,
}: {
status: FETCH_STATUS;
errorCode?: ErrorCode;
}) {
function getNoItemsMessage({ status }: { status: FETCH_STATUS }) {
// loading state
const isLoading =
status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
if (isLoading) {
return <LoadingStatePrompt />;
}

// A known error occured. Show specific error message
if (errorCode) {
return MLErrorMessages[errorCode];
}

// An unexpected error occurred. Show default error message
if (status === FETCH_STATUS.FAILURE) {
return i18n.translate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,96 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { showAlert } from './AnomalyDetectionSetupLink';

const dataWithJobs = {
hasLegacyJobs: false,
jobs: [
{ job_id: 'job1', environment: 'staging' },
{ job_id: 'job2', environment: 'production' },
],
};
const dataWithoutJobs = ({ jobs: [] } as unknown) as any;

describe('#showAlert', () => {
describe('when an environment is selected', () => {
it('should return true when there are no jobs', () => {
const result = showAlert(dataWithoutJobs, 'testing');
expect(result).toBe(true);
});
it('should return true when environment is not included in the jobs', () => {
const result = showAlert(dataWithJobs, 'testing');
expect(result).toBe(true);
import React from 'react';
import { render, fireEvent, wait } from '@testing-library/react';
import { MissingJobsAlert } from './AnomalyDetectionSetupLink';
import * as hooks from '../../../../hooks/useFetcher';

async function renderTooltipAnchor({
jobs,
environment,
}: {
jobs: Array<{ job_id: string; environment: string }>;
environment?: string;
}) {
// mock api response
jest.spyOn(hooks, 'useFetcher').mockReturnValue({
data: { jobs },
status: hooks.FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
});

const { baseElement, container } = render(
<MissingJobsAlert environment={environment} />
);

// hover tooltip anchor if it exists
const toolTipAnchor = container.querySelector('.euiToolTipAnchor') as any;
if (toolTipAnchor) {
fireEvent.mouseOver(toolTipAnchor);

// wait for tooltip text to be in the DOM
await wait(() => {
const toolTipText = baseElement.querySelector('.euiToolTipPopover')
?.textContent;
expect(toolTipText).not.toBe(undefined);
});
it('should return false when environment is included in the jobs', () => {
const result = showAlert(dataWithJobs, 'staging');
expect(result).toBe(false);
}

const toolTipText = baseElement.querySelector('.euiToolTipPopover')
?.textContent;

return { toolTipText, toolTipAnchor };
}

describe('MissingJobsAlert', () => {
describe('when no jobs exist', () => {
it('shows a warning', async () => {
const { toolTipText, toolTipAnchor } = await renderTooltipAnchor({
jobs: [],
});

expect(toolTipAnchor).toBeInTheDocument();
expect(toolTipText).toBe(
'Anomaly detection is not yet enabled. Click to continue setup.'
);
});
});

describe('there is no environment selected (All)', () => {
it('should return true when there are no jobs', () => {
const result = showAlert(dataWithoutJobs, undefined);
expect(result).toBe(true);
describe('when no jobs exists for the selected environment', () => {
it('shows a warning', async () => {
const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({
jobs: [{ environment: 'production', job_id: 'my_job_id' }],
environment: 'staging',
});

expect(toolTipAnchor).toBeInTheDocument();
expect(toolTipText).toBe(
'Anomaly detection is not yet enabled for the environment "staging". Click to continue setup.'
);
});
it('should return false when there are any number of jobs', () => {
const result = showAlert(dataWithJobs, undefined);
expect(result).toBe(false);
});

describe('when a job exists for the selected environment', () => {
it('does not show a warning', async () => {
const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({
jobs: [{ environment: 'production', job_id: 'my_job_id' }],
environment: 'production',
});

expect(toolTipAnchor).not.toBeInTheDocument();
expect(toolTipText).toBe(undefined);
});
});

describe('when a known error occurred', () => {
it('should return false', () => {
const data = ({
errorCode: 'MISSING_READ_PRIVILEGES',
} as unknown) as any;
const result = showAlert(data, undefined);
expect(result).toBe(false);
describe('when at least one job exists and no environment is selected', () => {
it('does not show a warning', async () => {
const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({
jobs: [{ environment: 'production', job_id: 'my_job_id' }],
});

expect(toolTipAnchor).not.toBeInTheDocument();
expect(toolTipText).toBe(undefined);
});
});
});
Loading

0 comments on commit aa68e3b

Please sign in to comment.