Skip to content

Commit dbce65d

Browse files
Merge branch 'master' into ml-migrate-router
2 parents b7248f5 + aa68e3b commit dbce65d

File tree

33 files changed

+7305
-1279
lines changed

33 files changed

+7305
-1279
lines changed

src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export function getVisualizeUrl(
155155
params: {
156156
field: field.name,
157157
size: parseInt(aggsTermSize, 10),
158-
orderBy: '2',
158+
orderBy: '1',
159159
},
160160
};
161161
}
@@ -169,7 +169,7 @@ export function getVisualizeUrl(
169169
query: state.query,
170170
vis: {
171171
type,
172-
aggs: [{ schema: 'metric', type: 'count', id: '2' }, agg],
172+
aggs: [{ schema: 'metric', type: 'count', id: '1' }, agg],
173173
},
174174
} as any),
175175
},

test/functional/apps/discover/_field_visualize.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
2727
const kibanaServer = getService('kibanaServer');
2828
const log = getService('log');
2929
const queryBar = getService('queryBar');
30-
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
30+
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'visualize']);
3131
const defaultSettings = {
3232
defaultIndex: 'logstash-*',
3333
};
@@ -48,6 +48,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
4848
await PageObjects.timePicker.setDefaultAbsoluteRange();
4949
});
5050

51+
it('should be able to visualize a field and save the visualization', async () => {
52+
await PageObjects.discover.findFieldByName('type');
53+
log.debug('visualize a type field');
54+
await PageObjects.discover.clickFieldListItemVisualize('type');
55+
await PageObjects.visualize.saveVisualizationExpectSuccess('Top 5 server types');
56+
});
57+
5158
it('should visualize a field in area chart', async () => {
5259
await PageObjects.discover.findFieldByName('phpmemory');
5360
log.debug('visualize a phpmemory field');

x-pack/plugins/apm/common/anomaly_detection.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,18 @@ export interface ServiceAnomalyStats {
1313
jobId?: string;
1414
}
1515

16-
export const MLErrorMessages: Record<ErrorCode, string> = {
17-
INSUFFICIENT_LICENSE: i18n.translate(
18-
'xpack.apm.anomaly_detection.error.insufficient_license',
16+
export const ML_ERRORS = {
17+
INVALID_LICENSE: i18n.translate(
18+
'xpack.apm.anomaly_detection.error.invalid_license',
1919
{
20-
defaultMessage:
21-
'You must have a platinum license to use Anomaly Detection',
20+
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.`,
2221
}
2322
),
2423
MISSING_READ_PRIVILEGES: i18n.translate(
2524
'xpack.apm.anomaly_detection.error.missing_read_privileges',
2625
{
2726
defaultMessage:
28-
'You must have "read" privileges to Machine Learning in order to view Anomaly Detection jobs',
27+
'You must have "read" privileges to Machine Learning and APM in order to view Anomaly Detection jobs',
2928
}
3029
),
3130
MISSING_WRITE_PRIVILEGES: i18n.translate(
@@ -47,16 +46,4 @@ export const MLErrorMessages: Record<ErrorCode, string> = {
4746
defaultMessage: 'Machine learning is not available in the selected space',
4847
}
4948
),
50-
UNEXPECTED: i18n.translate('xpack.apm.anomaly_detection.error.unexpected', {
51-
defaultMessage: 'An unexpected error occurred',
52-
}),
5349
};
54-
55-
export enum ErrorCode {
56-
INSUFFICIENT_LICENSE = 'INSUFFICIENT_LICENSE',
57-
MISSING_READ_PRIVILEGES = 'MISSING_READ_PRIVILEGES',
58-
MISSING_WRITE_PRIVILEGES = 'MISSING_WRITE_PRIVILEGES',
59-
ML_NOT_AVAILABLE = 'ML_NOT_AVAILABLE',
60-
ML_NOT_AVAILABLE_IN_SPACE = 'ML_NOT_AVAILABLE_IN_SPACE',
61-
UNEXPECTED = 'UNEXPECTED',
62-
}

x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
EuiEmptyPrompt,
2121
} from '@elastic/eui';
2222
import { i18n } from '@kbn/i18n';
23-
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
23+
import { ML_ERRORS } from '../../../../../common/anomaly_detection';
2424
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
2525
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
2626
import { createJobs } from './create_jobs';
@@ -65,7 +65,7 @@ export function AddEnvironments({
6565
<EuiPanel>
6666
<EuiEmptyPrompt
6767
iconType="warning"
68-
body={<>{MLErrorMessages.MISSING_WRITE_PRIVILEGES}</>}
68+
body={<>{ML_ERRORS.MISSING_WRITE_PRIVILEGES}</>}
6969
/>
7070
</EuiPanel>
7171
);

x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import { i18n } from '@kbn/i18n';
88
import { NotificationsStart } from 'kibana/public';
9-
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
109
import { callApmApi } from '../../../../services/rest/createCallApmApi';
1110

1211
const errorToastTitle = i18n.translate(
@@ -27,31 +26,19 @@ export async function createJobs({
2726
toasts: NotificationsStart['toasts'];
2827
}) {
2928
try {
30-
const res = await callApmApi({
29+
await callApmApi({
3130
pathname: '/api/apm/settings/anomaly-detection/jobs',
3231
method: 'POST',
3332
params: {
3433
body: { environments },
3534
},
3635
});
3736

38-
// a known error occurred
39-
if (res?.errorCode) {
40-
toasts.addDanger({
41-
title: errorToastTitle,
42-
text: MLErrorMessages[res.errorCode],
43-
});
44-
return false;
45-
}
46-
47-
// job created successfully
4837
toasts.addSuccess({
4938
title: successToastTitle,
5039
text: getSuccessToastMessage(environments),
5140
});
5241
return true;
53-
54-
// an unknown/unexpected error occurred
5542
} catch (error) {
5643
toasts.addDanger({
5744
title: errorToastTitle,

x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import React, { useState } from 'react';
88
import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui';
99
import { i18n } from '@kbn/i18n';
1010
import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui';
11-
import { MLErrorMessages } from '../../../../../common/anomaly_detection';
11+
import { ML_ERRORS } from '../../../../../common/anomaly_detection';
1212
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
1313
import { JobsList } from './jobs_list';
1414
import { AddEnvironments } from './add_environments';
@@ -25,7 +25,6 @@ export type AnomalyDetectionApiResponse = APIReturnType<
2525
const DEFAULT_VALUE: AnomalyDetectionApiResponse = {
2626
jobs: [],
2727
hasLegacyJobs: false,
28-
errorCode: undefined,
2928
};
3029

3130
export function AnomalyDetection() {
@@ -49,15 +48,7 @@ export function AnomalyDetection() {
4948
if (!hasValidLicense) {
5049
return (
5150
<EuiPanel>
52-
<LicensePrompt
53-
text={i18n.translate(
54-
'xpack.apm.settings.anomaly_detection.license.text',
55-
{
56-
defaultMessage:
57-
"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.",
58-
}
59-
)}
60-
/>
51+
<LicensePrompt text={ML_ERRORS.INVALID_LICENSE} />
6152
</EuiPanel>
6253
);
6354
}
@@ -67,7 +58,7 @@ export function AnomalyDetection() {
6758
<EuiPanel>
6859
<EuiEmptyPrompt
6960
iconType="warning"
70-
body={<>{MLErrorMessages.MISSING_READ_PRIVILEGES}</>}
61+
body={<>{ML_ERRORS.MISSING_READ_PRIVILEGES}</>}
7162
/>
7263
</EuiPanel>
7364
);

x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ import {
1616
} from '@elastic/eui';
1717
import { i18n } from '@kbn/i18n';
1818
import { FormattedMessage } from '@kbn/i18n/react';
19-
import {
20-
MLErrorMessages,
21-
ErrorCode,
22-
} from '../../../../../common/anomaly_detection';
2319
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
2420
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
2521
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
@@ -66,7 +62,7 @@ interface Props {
6662
onAddEnvironments: () => void;
6763
}
6864
export function JobsList({ data, status, onAddEnvironments }: Props) {
69-
const { jobs, hasLegacyJobs, errorCode } = data;
65+
const { jobs, hasLegacyJobs } = data;
7066

7167
return (
7268
<EuiPanel>
@@ -115,10 +111,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {
115111
</EuiText>
116112
<EuiSpacer size="l" />
117113
<ManagedTable
118-
noItemsMessage={getNoItemsMessage({
119-
status,
120-
errorCode,
121-
})}
114+
noItemsMessage={getNoItemsMessage({ status })}
122115
columns={columns}
123116
items={jobs}
124117
/>
@@ -129,25 +122,14 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {
129122
);
130123
}
131124

132-
function getNoItemsMessage({
133-
status,
134-
errorCode,
135-
}: {
136-
status: FETCH_STATUS;
137-
errorCode?: ErrorCode;
138-
}) {
125+
function getNoItemsMessage({ status }: { status: FETCH_STATUS }) {
139126
// loading state
140127
const isLoading =
141128
status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
142129
if (isLoading) {
143130
return <LoadingStatePrompt />;
144131
}
145132

146-
// A known error occured. Show specific error message
147-
if (errorCode) {
148-
return MLErrorMessages[errorCode];
149-
}
150-
151133
// An unexpected error occurred. Show default error message
152134
if (status === FETCH_STATUS.FAILURE) {
153135
return i18n.translate(

x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,96 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { showAlert } from './AnomalyDetectionSetupLink';
8-
9-
const dataWithJobs = {
10-
hasLegacyJobs: false,
11-
jobs: [
12-
{ job_id: 'job1', environment: 'staging' },
13-
{ job_id: 'job2', environment: 'production' },
14-
],
15-
};
16-
const dataWithoutJobs = ({ jobs: [] } as unknown) as any;
17-
18-
describe('#showAlert', () => {
19-
describe('when an environment is selected', () => {
20-
it('should return true when there are no jobs', () => {
21-
const result = showAlert(dataWithoutJobs, 'testing');
22-
expect(result).toBe(true);
23-
});
24-
it('should return true when environment is not included in the jobs', () => {
25-
const result = showAlert(dataWithJobs, 'testing');
26-
expect(result).toBe(true);
7+
import React from 'react';
8+
import { render, fireEvent, wait } from '@testing-library/react';
9+
import { MissingJobsAlert } from './AnomalyDetectionSetupLink';
10+
import * as hooks from '../../../../hooks/useFetcher';
11+
12+
async function renderTooltipAnchor({
13+
jobs,
14+
environment,
15+
}: {
16+
jobs: Array<{ job_id: string; environment: string }>;
17+
environment?: string;
18+
}) {
19+
// mock api response
20+
jest.spyOn(hooks, 'useFetcher').mockReturnValue({
21+
data: { jobs },
22+
status: hooks.FETCH_STATUS.SUCCESS,
23+
refetch: jest.fn(),
24+
});
25+
26+
const { baseElement, container } = render(
27+
<MissingJobsAlert environment={environment} />
28+
);
29+
30+
// hover tooltip anchor if it exists
31+
const toolTipAnchor = container.querySelector('.euiToolTipAnchor') as any;
32+
if (toolTipAnchor) {
33+
fireEvent.mouseOver(toolTipAnchor);
34+
35+
// wait for tooltip text to be in the DOM
36+
await wait(() => {
37+
const toolTipText = baseElement.querySelector('.euiToolTipPopover')
38+
?.textContent;
39+
expect(toolTipText).not.toBe(undefined);
2740
});
28-
it('should return false when environment is included in the jobs', () => {
29-
const result = showAlert(dataWithJobs, 'staging');
30-
expect(result).toBe(false);
41+
}
42+
43+
const toolTipText = baseElement.querySelector('.euiToolTipPopover')
44+
?.textContent;
45+
46+
return { toolTipText, toolTipAnchor };
47+
}
48+
49+
describe('MissingJobsAlert', () => {
50+
describe('when no jobs exist', () => {
51+
it('shows a warning', async () => {
52+
const { toolTipText, toolTipAnchor } = await renderTooltipAnchor({
53+
jobs: [],
54+
});
55+
56+
expect(toolTipAnchor).toBeInTheDocument();
57+
expect(toolTipText).toBe(
58+
'Anomaly detection is not yet enabled. Click to continue setup.'
59+
);
3160
});
3261
});
3362

34-
describe('there is no environment selected (All)', () => {
35-
it('should return true when there are no jobs', () => {
36-
const result = showAlert(dataWithoutJobs, undefined);
37-
expect(result).toBe(true);
63+
describe('when no jobs exists for the selected environment', () => {
64+
it('shows a warning', async () => {
65+
const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({
66+
jobs: [{ environment: 'production', job_id: 'my_job_id' }],
67+
environment: 'staging',
68+
});
69+
70+
expect(toolTipAnchor).toBeInTheDocument();
71+
expect(toolTipText).toBe(
72+
'Anomaly detection is not yet enabled for the environment "staging". Click to continue setup.'
73+
);
3874
});
39-
it('should return false when there are any number of jobs', () => {
40-
const result = showAlert(dataWithJobs, undefined);
41-
expect(result).toBe(false);
75+
});
76+
77+
describe('when a job exists for the selected environment', () => {
78+
it('does not show a warning', async () => {
79+
const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({
80+
jobs: [{ environment: 'production', job_id: 'my_job_id' }],
81+
environment: 'production',
82+
});
83+
84+
expect(toolTipAnchor).not.toBeInTheDocument();
85+
expect(toolTipText).toBe(undefined);
4286
});
4387
});
4488

45-
describe('when a known error occurred', () => {
46-
it('should return false', () => {
47-
const data = ({
48-
errorCode: 'MISSING_READ_PRIVILEGES',
49-
} as unknown) as any;
50-
const result = showAlert(data, undefined);
51-
expect(result).toBe(false);
89+
describe('when at least one job exists and no environment is selected', () => {
90+
it('does not show a warning', async () => {
91+
const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({
92+
jobs: [{ environment: 'production', job_id: 'my_job_id' }],
93+
});
94+
95+
expect(toolTipAnchor).not.toBeInTheDocument();
96+
expect(toolTipText).toBe(undefined);
5297
});
5398
});
5499
});

0 commit comments

Comments
 (0)