Skip to content

Commit

Permalink
Merge branch 'master' into ml-migrate-router
Browse files Browse the repository at this point in the history
  • Loading branch information
elasticmachine committed Jul 30, 2020
2 parents b7248f5 + aa68e3b commit dbce65d
Show file tree
Hide file tree
Showing 33 changed files with 7,305 additions and 1,279 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export function getVisualizeUrl(
params: {
field: field.name,
size: parseInt(aggsTermSize, 10),
orderBy: '2',
orderBy: '1',
},
};
}
Expand All @@ -169,7 +169,7 @@ export function getVisualizeUrl(
query: state.query,
vis: {
type,
aggs: [{ schema: 'metric', type: 'count', id: '2' }, agg],
aggs: [{ schema: 'metric', type: 'count', id: '1' }, agg],
},
} as any),
},
Expand Down
9 changes: 8 additions & 1 deletion test/functional/apps/discover/_field_visualize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const log = getService('log');
const queryBar = getService('queryBar');
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'visualize']);
const defaultSettings = {
defaultIndex: 'logstash-*',
};
Expand All @@ -48,6 +48,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.timePicker.setDefaultAbsoluteRange();
});

it('should be able to visualize a field and save the visualization', async () => {
await PageObjects.discover.findFieldByName('type');
log.debug('visualize a type field');
await PageObjects.discover.clickFieldListItemVisualize('type');
await PageObjects.visualize.saveVisualizationExpectSuccess('Top 5 server types');
});

it('should visualize a field in area chart', async () => {
await PageObjects.discover.findFieldByName('phpmemory');
log.debug('visualize a phpmemory field');
Expand Down
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 dbce65d

Please sign in to comment.