Merge branch '7.x' into backport/7.x/pr-78221

This commit is contained in:
Elastic Machine 2020-09-29 14:17:03 -04:00 committed by GitHub
commit 312cf73f11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 2970 additions and 3180 deletions

View file

@ -150,7 +150,7 @@
"boom": "^7.2.0",
"chalk": "^2.4.2",
"check-disk-space": "^2.1.0",
"chokidar": "3.2.1",
"chokidar": "^3.4.2",
"color": "1.0.3",
"commander": "^3.0.2",
"core-js": "^3.6.4",
@ -346,7 +346,7 @@
"angular-route": "^1.8.0",
"angular-sortable-view": "^0.0.17",
"archiver": "^3.1.1",
"axe-core": "^3.4.1",
"axe-core": "^4.0.2",
"babel-eslint": "^10.0.3",
"babel-jest": "^25.5.1",
"babel-plugin-istanbul": "^6.0.0",

File diff suppressed because it is too large Load diff

View file

@ -37,7 +37,7 @@
"babel-loader": "^8.0.6",
"brace": "0.11.1",
"chalk": "^4.1.0",
"chokidar": "3.2.1",
"chokidar": "^3.4.2",
"core-js": "^3.6.4",
"css-loader": "^3.4.2",
"expose-loader": "^0.7.5",

View file

@ -227,9 +227,6 @@ export class ClusterManager {
fromRoot('src/legacy/server'),
fromRoot('src/legacy/ui'),
fromRoot('src/legacy/utils'),
fromRoot('x-pack/legacy/common'),
fromRoot('x-pack/legacy/plugins'),
fromRoot('x-pack/legacy/server'),
fromRoot('config'),
...extraPaths,
].map((path) => resolve(path))
@ -242,7 +239,6 @@ export class ClusterManager {
/\.md$/,
/debug\.log$/,
...pluginInternalDirsIgnore,
fromRoot('src/legacy/server/sass/__tmp__'),
fromRoot('x-pack/plugins/reporting/chromium'),
fromRoot('x-pack/plugins/security_solution/cypress'),
fromRoot('x-pack/plugins/apm/e2e'),
@ -253,7 +249,6 @@ export class ClusterManager {
fromRoot('x-pack/plugins/lists/server/scripts'),
fromRoot('x-pack/plugins/security_solution/scripts'),
fromRoot('x-pack/plugins/security_solution/server/lib/detection_engine/scripts'),
'plugins/java_languageserver',
];
this.watcher = chokidar.watch(watchPaths, {

View file

@ -613,6 +613,7 @@ export class QueryStringInputUI extends Component<Props, State> {
})}
aria-haspopup="true"
aria-expanded={this.state.isSuggestionsVisible}
data-skip-axe="aria-required-children"
>
<div
role="search"

View file

@ -51,7 +51,17 @@ jest.mock('../../../kibana_services', () => ({
}),
}));
function getComponent(selected = false, showDetails = false, useShortDots = false) {
function getComponent({
selected = false,
showDetails = false,
useShortDots = false,
field,
}: {
selected?: boolean;
showDetails?: boolean;
useShortDots?: boolean;
field?: IndexPatternField;
}) {
const indexPattern = getStubIndexPattern(
'logstash-*',
(cfg: any) => cfg,
@ -60,23 +70,25 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals
coreMock.createSetup()
);
const field = new IndexPatternField(
{
name: 'bytes',
type: 'number',
esTypes: ['long'],
count: 10,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
'bytes'
);
const finalField =
field ??
new IndexPatternField(
{
name: 'bytes',
type: 'number',
esTypes: ['long'],
count: 10,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
'bytes'
);
const props = {
indexPattern,
field,
field: finalField,
getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })),
onAddFilter: jest.fn(),
onAddField: jest.fn(),
@ -91,18 +103,37 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals
describe('discover sidebar field', function () {
it('should allow selecting fields', function () {
const { comp, props } = getComponent();
const { comp, props } = getComponent({});
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
expect(props.onAddField).toHaveBeenCalledWith('bytes');
});
it('should allow deselecting fields', function () {
const { comp, props } = getComponent(true);
const { comp, props } = getComponent({ selected: true });
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
expect(props.onRemoveField).toHaveBeenCalledWith('bytes');
});
it('should trigger getDetails', function () {
const { comp, props } = getComponent(true);
const { comp, props } = getComponent({ selected: true });
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
expect(props.getDetails).toHaveBeenCalledWith(props.field);
});
it('should not allow clicking on _source', function () {
const field = new IndexPatternField(
{
name: '_source',
type: '_source',
esTypes: ['_source'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
'_source'
);
const { comp, props } = getComponent({
selected: true,
field,
});
findTestSubject(comp, 'field-_source-showDetails').simulate('click');
expect(props.getDetails).not.toHaveBeenCalled();
});
});

View file

@ -172,6 +172,19 @@ export function DiscoverField({
);
}
if (field.type === '_source') {
return (
<FieldButton
size="s"
className="dscSidebarItem"
dataTestSubj={`field-${field.name}-showDetails`}
fieldIcon={dscFieldIcon}
fieldAction={actionButton}
fieldName={fieldName}
/>
);
}
return (
<EuiPopover
ownFocus
@ -184,7 +197,7 @@ export function DiscoverField({
onClick={() => {
togglePopover();
}}
buttonProps={{ 'data-test-subj': `field-${field.name}-showDetails` }}
dataTestSubj={`field-${field.name}-showDetails`}
fieldIcon={dscFieldIcon}
fieldAction={actionButton}
fieldName={fieldName}

View file

@ -19,8 +19,7 @@
import './field_button.scss';
import classNames from 'classnames';
import React, { ReactNode, HTMLAttributes, ButtonHTMLAttributes } from 'react';
import { CommonProps } from '@elastic/eui';
import React, { ReactNode, HTMLAttributes } from 'react';
export interface FieldButtonProps extends HTMLAttributes<HTMLDivElement> {
/**
@ -54,13 +53,10 @@ export interface FieldButtonProps extends HTMLAttributes<HTMLDivElement> {
size?: ButtonSize;
className?: string;
/**
* The component always renders a `<button>` and therefore will always need an `onClick`
* The component will render a `<button>` when provided an `onClick`
*/
onClick: () => void;
/**
* Pass more button props to the actual `<button>` element
*/
buttonProps?: ButtonHTMLAttributes<HTMLButtonElement> & CommonProps;
onClick?: () => void;
dataTestSubj?: string;
}
const sizeToClassNameMap = {
@ -82,8 +78,7 @@ export function FieldButton({
className,
isDraggable = false,
onClick,
buttonProps,
...rest
dataTestSubj,
}: FieldButtonProps) {
const classes = classNames(
'kbnFieldButton',
@ -93,27 +88,31 @@ export function FieldButton({
className
);
const buttonClasses = classNames(
'kbn-resetFocusState kbnFieldButton__button',
buttonProps && buttonProps.className
);
return (
<div className={classes} {...rest}>
<button
onClick={(e) => {
if (e.type === 'click') {
e.currentTarget.focus();
}
onClick();
}}
{...buttonProps}
className={buttonClasses}
>
{fieldIcon && <span className="kbnFieldButton__fieldIcon">{fieldIcon}</span>}
{fieldName && <span className="kbnFieldButton__name">{fieldName}</span>}
{fieldInfoIcon && <div className="kbnFieldButton__infoIcon">{fieldInfoIcon}</div>}
</button>
<div className={classes}>
{onClick ? (
<button
onClick={(e) => {
if (e.type === 'click') {
e.currentTarget.focus();
}
onClick();
}}
data-test-subj={dataTestSubj}
className={'kbn-resetFocusState kbnFieldButton__button'}
>
{fieldIcon && <span className="kbnFieldButton__fieldIcon">{fieldIcon}</span>}
{fieldName && <span className="kbnFieldButton__name">{fieldName}</span>}
{fieldInfoIcon && <div className="kbnFieldButton__infoIcon">{fieldInfoIcon}</div>}
</button>
) : (
<div className={'kbn-resetFocusState kbnFieldButton__button'} data-test-subj={dataTestSubj}>
{fieldIcon && <span className="kbnFieldButton__fieldIcon">{fieldIcon}</span>}
{fieldName && <span className="kbnFieldButton__name">{fieldName}</span>}
{fieldInfoIcon && <div className="kbnFieldButton__infoIcon">{fieldInfoIcon}</div>}
</div>
)}
{fieldAction && <div className="kbnFieldButton__fieldAction">{fieldAction}</div>}
</div>
);

View file

@ -147,7 +147,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
<div
aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further."
class="euiModal euiModal--maxWidth-default visNewVisDialog"
role="menu"
tabindex="0"
>
<button
@ -251,7 +250,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
class="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visWithAliasUrl"
data-vis-stage="alias"
role="menuitem"
type="button"
>
<div
@ -283,7 +281,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
class="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visWithSearch"
data-vis-stage="production"
role="menuitem"
type="button"
>
<div
@ -316,7 +313,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
data-test-subj="visType-vis"
data-vis-stage="production"
disabled=""
role="menuitem"
type="button"
>
<div
@ -377,7 +373,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further."
className="visNewVisDialog"
onClose={[Function]}
role="menu"
>
<EuiFocusTrap>
<div
@ -387,7 +382,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further."
className="euiModal euiModal--maxWidth-default visNewVisDialog"
onKeyDown={[Function]}
role="menu"
tabIndex={0}
>
<EuiI18n
@ -649,7 +643,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
>
<button
aria-describedby="visTypeDescription-visWithAliasUrl"
@ -662,7 +655,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
type="button"
>
<div
@ -720,7 +712,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
>
<button
aria-describedby="visTypeDescription-visWithSearch"
@ -733,7 +724,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
type="button"
>
<div
@ -791,7 +781,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
>
<button
aria-describedby="visTypeDescription-vis"
@ -804,7 +793,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
type="button"
>
<div
@ -1060,7 +1048,6 @@ exports[`NewVisModal should render as expected 1`] = `
<div
aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further."
class="euiModal euiModal--maxWidth-default visNewVisDialog"
role="menu"
tabindex="0"
>
<button
@ -1148,7 +1135,6 @@ exports[`NewVisModal should render as expected 1`] = `
class="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-vis"
data-vis-stage="production"
role="menuitem"
type="button"
>
<div
@ -1180,7 +1166,6 @@ exports[`NewVisModal should render as expected 1`] = `
class="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visWithAliasUrl"
data-vis-stage="alias"
role="menuitem"
type="button"
>
<div
@ -1212,7 +1197,6 @@ exports[`NewVisModal should render as expected 1`] = `
class="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visWithSearch"
data-vis-stage="production"
role="menuitem"
type="button"
>
<div
@ -1273,7 +1257,6 @@ exports[`NewVisModal should render as expected 1`] = `
aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further."
className="visNewVisDialog"
onClose={[Function]}
role="menu"
>
<EuiFocusTrap>
<div
@ -1283,7 +1266,6 @@ exports[`NewVisModal should render as expected 1`] = `
aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further."
className="euiModal euiModal--maxWidth-default visNewVisDialog"
onKeyDown={[Function]}
role="menu"
tabIndex={0}
>
<EuiI18n
@ -1494,7 +1476,6 @@ exports[`NewVisModal should render as expected 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
>
<button
aria-describedby="visTypeDescription-vis"
@ -1507,7 +1488,6 @@ exports[`NewVisModal should render as expected 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
type="button"
>
<div
@ -1565,7 +1545,6 @@ exports[`NewVisModal should render as expected 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
>
<button
aria-describedby="visTypeDescription-visWithAliasUrl"
@ -1578,7 +1557,6 @@ exports[`NewVisModal should render as expected 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
type="button"
>
<div
@ -1636,7 +1614,6 @@ exports[`NewVisModal should render as expected 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
>
<button
aria-describedby="visTypeDescription-visWithSearch"
@ -1649,7 +1626,6 @@ exports[`NewVisModal should render as expected 1`] = `
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
type="button"
>
<div

View file

@ -108,7 +108,6 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
onClose={this.onCloseModal}
className="visNewVisDialog"
aria-label={visNewVisDialogAriaLabel}
role="menu"
>
<TypeSelection
showExperimental={this.isLabsEnabled}

View file

@ -259,7 +259,6 @@ class TypeSelection extends React.Component<TypeSelectionProps, TypeSelectionSta
data-vis-stage={!('aliasPath' in visType) ? visType.stage : 'alias'}
disabled={isDisabled}
aria-describedby={`visTypeDescription-${visType.name}`}
role="menuitem"
{...stage}
>
<VisTypeIcon

View file

@ -36,6 +36,8 @@ interface TestOptions {
export const normalizeResult = (report: any) => {
if (report.error) {
const error = new Error(report.error.message);
error.stack = report.error.stack;
throw report.error;
}
@ -71,7 +73,6 @@ export function A11yProvider({ getService }: FtrProviderContext) {
.concat(excludeTestSubj || [])
.map((ts) => [testSubjectToCss(ts)])
.concat([
['.ace_scrollbar'],
[
'.leaflet-vega-container[role="graphics-document"][aria-roledescription="visualization"]',
],
@ -97,7 +98,7 @@ export function A11yProvider({ getService }: FtrProviderContext) {
runOnly: ['wcag2a', 'wcag2aa'],
rules: {
'color-contrast': {
enabled: false,
enabled: false, // disabled because we have too many failures
},
bypass: {
enabled: false, // disabled because it's too flaky

View file

@ -23,6 +23,18 @@ export function analyzeWithAxe(context, options, callback) {
Promise.resolve()
.then(() => {
if (window.axe) {
window.axe.configure({
rules: [
{
id: 'scrollable-region-focusable',
selector: '[data-skip-axe="scrollable-region-focusable"]',
},
{
id: 'aria-required-children',
selector: '[data-skip-axe="aria-required-children"] > *',
},
],
});
return window.axe.run(context, options);
}
@ -31,7 +43,14 @@ export function analyzeWithAxe(context, options, callback) {
})
.then(
(result) => callback({ result }),
(error) => callback({ error })
(error) => {
callback({
error: {
message: error.message,
stack: error.stack,
},
});
}
);
}

View file

@ -90,7 +90,6 @@ export function PaletteLegends({
<StyledSpan darkMode={darkMode}>
<PaletteLegend color={color}>
<EuiText size="xs">
{labels[ind]} ({ranks?.[ind]}%)
<FormattedMessage
id="xpack.apm.rum.coreVitals.paletteLegend.rankPercentage"
defaultMessage="{labelsInd} ({ranksInd}%)"

View file

@ -5,8 +5,6 @@
*/
import { getFormattedBuckets } from '../index';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IBucket } from '../../../../../../server/lib/transactions/distribution/get_buckets/transform';
describe('Distribution', () => {
it('getFormattedBuckets', () => {
@ -20,6 +18,7 @@ describe('Distribution', () => {
samples: [
{
transactionId: 'someTransactionId',
traceId: 'someTraceId',
},
],
},
@ -29,10 +28,12 @@ describe('Distribution', () => {
samples: [
{
transactionId: 'anotherTransactionId',
traceId: 'anotherTraceId',
},
],
},
] as IBucket[];
];
expect(getFormattedBuckets(buckets, 20)).toEqual([
{ x: 20, x0: 0, y: 0, style: { cursor: 'default' } },
{ x: 40, x0: 20, y: 0, style: { cursor: 'default' } },

View file

@ -13,7 +13,7 @@ import { ValuesType } from 'utility-types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform';
import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { getDurationFormatter } from '../../../../utils/formatters';
// @ts-expect-error
@ -30,7 +30,10 @@ interface IChartPoint {
};
}
export function getFormattedBuckets(buckets: IBucket[], bucketSize: number) {
export function getFormattedBuckets(
buckets: DistributionBucket[],
bucketSize: number
) {
if (!buckets) {
return [];
}

View file

@ -18,7 +18,7 @@ import { Location } from 'history';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform';
import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
@ -34,7 +34,7 @@ interface Props {
waterfall: IWaterfall;
exceedsMax: boolean;
isLoading: boolean;
traceSamples: IBucket['samples'];
traceSamples: DistributionBucket['samples'];
}
export function WaterfallWithSummmary({

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { flatten, omit } from 'lodash';
import { flatten, omit, isEmpty } from 'lodash';
import { useHistory, useParams } from 'react-router-dom';
import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
@ -69,11 +69,11 @@ export function useTransactionDistribution(urlParams: IUrlParams) {
// selected sample was not found. select a new one:
// sorted by total number of requests, but only pick
// from buckets that have samples
const preferredSample = maybe(
response.buckets
.filter((bucket) => bucket.samples.length > 0)
.sort((bucket) => bucket.count)[0]?.samples[0]
);
const bucketsSortedByCount = response.buckets
.filter((bucket) => !isEmpty(bucket.samples))
.sort((bucket) => bucket.count);
const preferredSample = maybe(bucketsSortedByCount[0]?.samples[0]);
history.push({
...history.location,

View file

@ -639,7 +639,7 @@ Object {
"body": Object {
"aggs": Object {
"stats": Object {
"extended_stats": Object {
"max": Object {
"field": "transaction.duration.us",
},
},

View file

@ -1,91 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ProcessorEvent } from '../../../../../common/processor_event';
import {
SERVICE_NAME,
TRACE_ID,
TRANSACTION_DURATION,
TRANSACTION_ID,
TRANSACTION_NAME,
TRANSACTION_SAMPLED,
TRANSACTION_TYPE,
} from '../../../../../common/elasticsearch_fieldnames';
import { rangeFilter } from '../../../../../common/utils/range_filter';
import {
Setup,
SetupTimeRange,
SetupUIFilters,
} from '../../../helpers/setup_request';
export async function bucketFetcher(
serviceName: string,
transactionName: string,
transactionType: string,
transactionId: string,
traceId: string,
distributionMax: number,
bucketSize: number,
setup: Setup & SetupTimeRange & SetupUIFilters
) {
const { start, end, uiFiltersES, apmEventClient } = setup;
const params = {
apm: {
events: [ProcessorEvent.transaction as const],
},
body: {
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [TRANSACTION_TYPE]: transactionType } },
{ term: { [TRANSACTION_NAME]: transactionName } },
{ range: rangeFilter(start, end) },
...uiFiltersES,
],
should: [
{ term: { [TRACE_ID]: traceId } },
{ term: { [TRANSACTION_ID]: transactionId } },
],
},
},
aggs: {
distribution: {
histogram: {
field: TRANSACTION_DURATION,
interval: bucketSize,
min_doc_count: 0,
extended_bounds: {
min: 0,
max: distributionMax,
},
},
aggs: {
samples: {
filter: {
term: { [TRANSACTION_SAMPLED]: true },
},
aggs: {
items: {
top_hits: {
_source: [TRANSACTION_ID, TRACE_ID],
size: 10,
},
},
},
},
},
},
},
},
};
const response = await apmEventClient.search(params);
return response;
}

View file

@ -3,35 +3,204 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ValuesType } from 'utility-types';
import { PromiseReturnType } from '../../../../../typings/common';
import { joinByKey } from '../../../../../common/utils/join_by_key';
import { ProcessorEvent } from '../../../../../common/processor_event';
import {
SERVICE_NAME,
TRACE_ID,
TRANSACTION_DURATION,
TRANSACTION_ID,
TRANSACTION_NAME,
TRANSACTION_SAMPLED,
TRANSACTION_TYPE,
} from '../../../../../common/elasticsearch_fieldnames';
import { rangeFilter } from '../../../../../common/utils/range_filter';
import {
Setup,
SetupTimeRange,
SetupUIFilters,
} from '../../../helpers/setup_request';
import { bucketFetcher } from './fetcher';
import { bucketTransformer } from './transform';
import {
getDocumentTypeFilterForAggregatedTransactions,
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
} from '../../../helpers/aggregated_transactions';
export async function getBuckets(
serviceName: string,
transactionName: string,
transactionType: string,
transactionId: string,
traceId: string,
distributionMax: number,
bucketSize: number,
setup: Setup & SetupTimeRange & SetupUIFilters
) {
const response = await bucketFetcher(
serviceName,
transactionName,
transactionType,
transactionId,
traceId,
distributionMax,
bucketSize,
setup
);
return bucketTransformer(response);
function getHistogramAggOptions({
bucketSize,
field,
distributionMax,
}: {
bucketSize: number;
field: string;
distributionMax: number;
}) {
return {
field,
interval: bucketSize,
min_doc_count: 0,
extended_bounds: {
min: 0,
max: distributionMax,
},
};
}
export async function getBuckets({
serviceName,
transactionName,
transactionType,
transactionId,
traceId,
distributionMax,
bucketSize,
setup,
searchAggregatedTransactions,
}: {
serviceName: string;
transactionName: string;
transactionType: string;
transactionId: string;
traceId: string;
distributionMax: number;
bucketSize: number;
setup: Setup & SetupTimeRange & SetupUIFilters;
searchAggregatedTransactions: boolean;
}) {
const { start, end, uiFiltersES, apmEventClient } = setup;
const commonFilters = [
{ term: { [SERVICE_NAME]: serviceName } },
{ term: { [TRANSACTION_TYPE]: transactionType } },
{ term: { [TRANSACTION_NAME]: transactionName } },
{ range: rangeFilter(start, end) },
...uiFiltersES,
];
async function getSamplesForDistributionBuckets() {
const response = await apmEventClient.search({
apm: {
events: [ProcessorEvent.transaction],
},
body: {
query: {
bool: {
filter: [
...commonFilters,
{ term: { [TRANSACTION_SAMPLED]: true } },
],
should: [
{ term: { [TRACE_ID]: traceId } },
{ term: { [TRANSACTION_ID]: transactionId } },
],
},
},
aggs: {
distribution: {
histogram: getHistogramAggOptions({
bucketSize,
field: TRANSACTION_DURATION,
distributionMax,
}),
aggs: {
samples: {
top_hits: {
_source: [TRANSACTION_ID, TRACE_ID],
size: 10,
sort: {
_score: 'desc',
},
},
},
},
},
},
},
});
return (
response.aggregations?.distribution.buckets.map((bucket) => {
return {
key: bucket.key,
samples: bucket.samples.hits.hits.map((hit) => ({
traceId: hit._source.trace.id,
transactionId: hit._source.transaction.id,
})),
};
}) ?? []
);
}
async function getDistributionBuckets() {
const response = await apmEventClient.search({
apm: {
events: [
getProcessorEventForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
body: {
query: {
bool: {
filter: [
...commonFilters,
...getDocumentTypeFilterForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
},
aggs: {
distribution: {
histogram: getHistogramAggOptions({
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
bucketSize,
distributionMax,
}),
},
},
},
});
return (
response.aggregations?.distribution.buckets.map((bucket) => {
return {
key: bucket.key,
count: bucket.doc_count,
};
}) ?? []
);
}
const [
samplesForDistributionBuckets,
distributionBuckets,
] = await Promise.all([
getSamplesForDistributionBuckets(),
getDistributionBuckets(),
]);
const buckets = joinByKey(
[...samplesForDistributionBuckets, ...distributionBuckets],
'key'
).map((bucket) => ({
...bucket,
samples: bucket.samples ?? [],
count: bucket.count ?? 0,
}));
return {
noHits: buckets.length === 0,
bucketSize,
buckets,
};
}
export type DistributionBucket = ValuesType<
PromiseReturnType<typeof getBuckets>['buckets']
>;

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PromiseReturnType } from '../../../../../../observability/typings/common';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
import { bucketFetcher } from './fetcher';
type DistributionBucketResponse = PromiseReturnType<typeof bucketFetcher>;
export type IBucket = ReturnType<typeof getBucket>;
function getBucket(
bucket: Required<
DistributionBucketResponse
>['aggregations']['distribution']['buckets'][0]
) {
const samples = bucket.samples.items.hits.hits.map(
({ _source }: { _source: Transaction }) => ({
traceId: _source.trace.id,
transactionId: _source.transaction.id,
})
);
return {
key: bucket.key,
count: bucket.doc_count,
samples,
};
}
export function bucketTransformer(response: DistributionBucketResponse) {
const buckets =
response.aggregations?.distribution.buckets.map(getBucket) || [];
return {
noHits: response.hits.total.value === 0,
buckets,
};
}

View file

@ -4,10 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ProcessorEvent } from '../../../../common/processor_event';
import {
SERVICE_NAME,
TRANSACTION_DURATION,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
@ -16,18 +14,33 @@ import {
SetupTimeRange,
SetupUIFilters,
} from '../../helpers/setup_request';
import {
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
} from '../../helpers/aggregated_transactions';
export async function getDistributionMax(
serviceName: string,
transactionName: string,
transactionType: string,
setup: Setup & SetupTimeRange & SetupUIFilters
) {
export async function getDistributionMax({
serviceName,
transactionName,
transactionType,
setup,
searchAggregatedTransactions,
}: {
serviceName: string;
transactionName: string;
transactionType: string;
setup: Setup & SetupTimeRange & SetupUIFilters;
searchAggregatedTransactions: boolean;
}) {
const { start, end, uiFiltersES, apmEventClient } = setup;
const params = {
apm: {
events: [ProcessorEvent.transaction],
events: [
getProcessorEventForAggregatedTransactions(
searchAggregatedTransactions
),
],
},
body: {
size: 0,
@ -52,8 +65,10 @@ export async function getDistributionMax(
},
aggs: {
stats: {
extended_stats: {
field: TRANSACTION_DURATION,
max: {
field: getTransactionDurationFieldForAggregatedTransactions(
searchAggregatedTransactions
),
},
},
},
@ -61,5 +76,5 @@ export async function getDistributionMax(
};
const resp = await apmEventClient.search(params);
return resp.aggregations ? resp.aggregations.stats.max : null;
return resp.aggregations?.stats.value ?? null;
}

View file

@ -32,6 +32,7 @@ export async function getTransactionDistribution({
transactionId,
traceId,
setup,
searchAggregatedTransactions,
}: {
serviceName: string;
transactionName: string;
@ -39,20 +40,23 @@ export async function getTransactionDistribution({
transactionId: string;
traceId: string;
setup: Setup & SetupTimeRange & SetupUIFilters;
searchAggregatedTransactions: boolean;
}) {
const distributionMax = await getDistributionMax(
const distributionMax = await getDistributionMax({
serviceName,
transactionName,
transactionType,
setup
);
setup,
searchAggregatedTransactions,
});
if (distributionMax == null) {
return { noHits: true, buckets: [], bucketSize: 0 };
}
const bucketSize = getBucketSize(distributionMax);
const { buckets, noHits } = await getBuckets(
const { buckets, noHits } = await getBuckets({
serviceName,
transactionName,
transactionType,
@ -60,8 +64,9 @@ export async function getTransactionDistribution({
traceId,
distributionMax,
bucketSize,
setup
);
setup,
searchAggregatedTransactions,
});
return {
noHits,

View file

@ -102,6 +102,7 @@ describe('transaction queries', () => {
traceId: 'qux',
transactionId: 'quz',
setup,
searchAggregatedTransactions: false,
})
);

View file

@ -124,6 +124,10 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({
traceId = '',
} = context.params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
return getTransactionDistribution({
serviceName,
transactionType,
@ -131,6 +135,7 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({
transactionId,
traceId,
setup,
searchAggregatedTransactions,
});
},
}));

View file

@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { mockHistory } from './';
export const mockKibanaValues = {
config: { host: 'http://localhost:3002' },
history: mockHistory,
navigateToUrl: jest.fn(),
setBreadcrumbs: jest.fn(),
setDocTitle: jest.fn(),

View file

@ -15,7 +15,7 @@ export const mockHistory = {
pathname: '/current-path',
},
listen: jest.fn(() => jest.fn()),
};
} as any;
export const mockLocation = {
key: 'someKey',
pathname: '/current-path',
@ -25,6 +25,7 @@ export const mockLocation = {
};
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom') as object),
useHistory: jest.fn(() => mockHistory),
useLocation: jest.fn(() => mockLocation),
}));

View file

@ -41,6 +41,7 @@ export const renderApp = (
const unmountKibanaLogic = mountKibanaLogic({
config,
history: params.history,
navigateToUrl: core.application.navigateToUrl,
setBreadcrumbs: core.chrome.setBreadcrumbs,
setDocTitle: core.chrome.docTitle.change,
@ -53,9 +54,7 @@ export const renderApp = (
errorConnecting,
readOnlyMode: initialData.readOnlyMode,
});
const unmountFlashMessagesLogic = mountFlashMessagesLogic({
history: params.history,
});
const unmountFlashMessagesLogic = mountFlashMessagesLogic();
ReactDOM.render(
<I18nProvider>

View file

@ -7,11 +7,14 @@
import { resetContext } from 'kea';
import { mockHistory } from '../../__mocks__';
jest.mock('../kibana', () => ({
KibanaLogic: { values: { history: mockHistory } },
}));
import { FlashMessagesLogic, mountFlashMessagesLogic, IFlashMessage } from './';
describe('FlashMessagesLogic', () => {
const mount = () => mountFlashMessagesLogic({ history: mockHistory as any });
const mount = () => mountFlashMessagesLogic();
beforeEach(() => {
jest.clearAllMocks();

View file

@ -6,7 +6,8 @@
import { kea, MakeLogicType } from 'kea';
import { ReactNode } from 'react';
import { History } from 'history';
import { KibanaLogic } from '../kibana';
export interface IFlashMessage {
type: 'success' | 'info' | 'warning' | 'error';
@ -61,10 +62,10 @@ export const FlashMessagesLogic = kea<MakeLogicType<IFlashMessagesValues, IFlash
},
],
},
events: ({ props, values, actions }) => ({
events: ({ values, actions }) => ({
afterMount: () => {
// On React Router navigation, clear previous flash messages and load any queued messages
const unlisten = props.history.listen(() => {
const unlisten = KibanaLogic.values.history.listen(() => {
actions.clearFlashMessages();
actions.setFlashMessages(values.queuedMessages);
actions.clearQueuedMessages();
@ -81,11 +82,7 @@ export const FlashMessagesLogic = kea<MakeLogicType<IFlashMessagesValues, IFlash
/**
* Mount/props helper
*/
interface IFlashMessagesLogicProps {
history: History;
}
export const mountFlashMessagesLogic = (props: IFlashMessagesLogicProps) => {
FlashMessagesLogic(props);
export const mountFlashMessagesLogic = () => {
const unmount = FlashMessagesLogic.mount();
return unmount;
};

View file

@ -5,6 +5,9 @@
*/
import { mockHistory } from '../../__mocks__';
jest.mock('../kibana', () => ({
KibanaLogic: { values: { history: mockHistory } },
}));
import {
FlashMessagesLogic,
@ -18,7 +21,7 @@ describe('Flash Message Helpers', () => {
const message = 'I am a message';
beforeEach(() => {
mountFlashMessagesLogic({ history: mockHistory as any });
mountFlashMessagesLogic();
});
it('setSuccessMessage()', () => {

View file

@ -20,7 +20,10 @@ describe('KibanaLogic', () => {
it('sets values from props', () => {
mountKibanaLogic(mockKibanaValues);
expect(KibanaLogic.values).toEqual(mockKibanaValues);
expect(KibanaLogic.values).toEqual({
...mockKibanaValues,
navigateToUrl: expect.any(Function),
});
});
it('gracefully handles missing configs', () => {
@ -29,4 +32,20 @@ describe('KibanaLogic', () => {
expect(KibanaLogic.values.config).toEqual({});
});
});
describe('navigateToUrl()', () => {
beforeEach(() => mountKibanaLogic(mockKibanaValues));
it('runs paths through createHref before calling navigateToUrl', () => {
KibanaLogic.values.navigateToUrl('/test');
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test');
});
it('does not run paths through createHref if the shouldNotCreateHref option is passed', () => {
KibanaLogic.values.navigateToUrl('/test', { shouldNotCreateHref: true });
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test');
});
});
});

View file

@ -6,26 +6,40 @@
import { kea, MakeLogicType } from 'kea';
import { History } from 'history';
import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public';
export interface IKibanaValues {
import { createHref, ICreateHrefOptions } from '../react_router_helpers';
interface IKibanaLogicProps {
config: { host?: string };
history: History;
navigateToUrl: ApplicationStart['navigateToUrl'];
setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void;
setDocTitle(title: string): void;
}
export interface IKibanaValues extends IKibanaLogicProps {
navigateToUrl(path: string, options?: ICreateHrefOptions): Promise<void>;
}
export const KibanaLogic = kea<MakeLogicType<IKibanaValues>>({
path: ['enterprise_search', 'kibana_logic'],
reducers: ({ props }) => ({
config: [props.config || {}, {}],
navigateToUrl: [props.navigateToUrl, {}],
history: [props.history, {}],
navigateToUrl: [
(url: string, options?: ICreateHrefOptions) => {
const href = createHref(url, props.history, options);
return props.navigateToUrl(href);
},
{},
],
setBreadcrumbs: [props.setBreadcrumbs, {}],
setDocTitle: [props.setDocTitle, {}],
}),
});
export const mountKibanaLogic = (props: IKibanaValues) => {
export const mountKibanaLogic = (props: IKibanaLogicProps) => {
KibanaLogic(props);
const unmount = KibanaLogic.mount();
return unmount;

View file

@ -5,10 +5,12 @@
*/
import '../../__mocks__/kea.mock';
import '../../__mocks__/react_router_history.mock';
import { mockKibanaValues, mockHistory } from '../../__mocks__';
jest.mock('../react_router_helpers', () => ({ letBrowserHandleEvent: jest.fn(() => false) }));
jest.mock('../react_router_helpers', () => ({
letBrowserHandleEvent: jest.fn(() => false),
createHref: jest.requireActual('../react_router_helpers').createHref,
}));
import { letBrowserHandleEvent } from '../react_router_helpers';
import {
@ -50,21 +52,23 @@ describe('useBreadcrumbs', () => {
it('prevents default navigation and uses React Router history on click', () => {
const breadcrumb = useBreadcrumbs([{ text: '', path: '/test' }])[0] as any;
expect(breadcrumb.href).toEqual('/app/enterprise_search/test');
expect(mockHistory.createHref).toHaveBeenCalled();
const event = { preventDefault: jest.fn() };
breadcrumb.onClick(event);
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test');
expect(mockHistory.createHref).toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled();
});
it('does not call createHref if shouldNotCreateHref is passed', () => {
const breadcrumb = useBreadcrumbs([
{ text: '', path: '/test', shouldNotCreateHref: true },
])[0] as any;
breadcrumb.onClick({ preventDefault: () => null });
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test');
expect(breadcrumb.href).toEqual('/test');
expect(mockHistory.createHref).not.toHaveBeenCalled();
});

View file

@ -5,7 +5,6 @@
*/
import { useValues } from 'kea';
import { useHistory } from 'react-router-dom';
import { EuiBreadcrumb } from '@elastic/eui';
import { KibanaLogic } from '../../shared/kibana';
@ -16,7 +15,7 @@ import {
WORKPLACE_SEARCH_PLUGIN,
} from '../../../../common/constants';
import { letBrowserHandleEvent } from '../react_router_helpers';
import { letBrowserHandleEvent, createHref } from '../react_router_helpers';
/**
* Generate React-Router-friendly EUI breadcrumb objects
@ -33,20 +32,17 @@ interface IBreadcrumb {
export type TBreadcrumbs = IBreadcrumb[];
export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => {
const history = useHistory();
const { navigateToUrl } = useValues(KibanaLogic);
const { navigateToUrl, history } = useValues(KibanaLogic);
return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => {
const breadcrumb = { text } as EuiBreadcrumb;
if (path) {
const href = shouldNotCreateHref ? path : (history.createHref({ pathname: path }) as string);
breadcrumb.href = href;
breadcrumb.href = createHref(path, history, { shouldNotCreateHref });
breadcrumb.onClick = (event) => {
if (letBrowserHandleEvent(event)) return;
event.preventDefault();
navigateToUrl(href);
navigateToUrl(path, { shouldNotCreateHref });
};
}

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { mockHistory } from '../../__mocks__';
import { createHref } from './';
describe('createHref', () => {
it('generates a path with the React Router basename included', () => {
expect(createHref('/test', mockHistory)).toEqual('/app/enterprise_search/test');
});
it('does not include the basename if shouldNotCreateHref is passed', () => {
expect(createHref('/test', mockHistory, { shouldNotCreateHref: true })).toEqual('/test');
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { History } from 'history';
/**
* This helper uses React Router's createHref function to generate links with router basenames accounted for.
* For example, if we perform navigateToUrl('/engines') within App Search, we expect the app basename
* to be taken into account to be intelligently routed to '/app/enterprise_search/app_search/engines'.
*
* This helper accomplishes that, while still giving us an escape hatch for navigation *between* apps.
* For example, if we want to navigate the user from App Search to Enterprise Search we could
* navigateToUrl('/app/enterprise_search', { shouldNotCreateHref: true })
*/
export interface ICreateHrefOptions {
shouldNotCreateHref?: boolean;
}
export const createHref = (
path: string,
history: History,
options?: ICreateHrefOptions
): string => {
return options?.shouldNotCreateHref ? path : history.createHref({ pathname: path });
};

View file

@ -5,7 +5,6 @@
*/
import '../../__mocks__/kea.mock';
import '../../__mocks__/react_router_history.mock';
import React from 'react';
import { shallow, mount } from 'enzyme';

View file

@ -6,11 +6,10 @@
import React from 'react';
import { useValues } from 'kea';
import { useHistory } from 'react-router-dom';
import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui';
import { KibanaLogic } from '../../shared/kibana';
import { letBrowserHandleEvent } from './link_events';
import { letBrowserHandleEvent, createHref } from './';
/**
* Generates either an EuiLink or EuiButton with a React-Router-ified link
@ -33,11 +32,10 @@ export const EuiReactRouterHelper: React.FC<IEuiReactRouterProps> = ({
shouldNotCreateHref,
children,
}) => {
const history = useHistory();
const { navigateToUrl } = useValues(KibanaLogic);
const { navigateToUrl, history } = useValues(KibanaLogic);
// Generate the correct link href (with basename etc. accounted for)
const href = shouldNotCreateHref ? to : history.createHref({ pathname: to });
const href = createHref(to, history, { shouldNotCreateHref });
const reactRouterLinkClick = (event: React.MouseEvent) => {
if (onClick) onClick(); // Run any passed click events (e.g. telemetry)
@ -47,7 +45,7 @@ export const EuiReactRouterHelper: React.FC<IEuiReactRouterProps> = ({
event.preventDefault();
// Perform SPA navigation.
navigateToUrl(href);
navigateToUrl(to, { shouldNotCreateHref });
};
const reactRouterProps = { href, onClick: reactRouterLinkClick };

View file

@ -5,5 +5,6 @@
*/
export { letBrowserHandleEvent } from './link_events';
export { createHref, ICreateHrefOptions } from './create_href';
export { EuiReactRouterLink as EuiLink } from './eui_link';
export { EuiReactRouterButton as EuiButton } from './eui_link';

View file

@ -161,6 +161,9 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) {
defaultMessage: 'Search Elastic',
}),
}}
popoverProps={{
repositionOnScroll: true,
}}
emptyMessage={
<EuiSelectableMessage style={{ minHeight: 300 }}>
<p>

View file

@ -11,7 +11,7 @@ import { useCore } from './use_core';
const BASE_BREADCRUMB: ChromeBreadcrumb = {
href: pagePathGetters.overview(),
text: i18n.translate('xpack.ingestManager.breadcrumbs.appTitle', {
defaultMessage: 'Ingest Manager',
defaultMessage: 'Fleet',
}),
};
@ -155,21 +155,15 @@ const breadcrumbGetters: {
fleet: () => [
BASE_BREADCRUMB,
{
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', {
defaultMessage: 'Fleet',
text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', {
defaultMessage: 'Agents',
}),
},
],
fleet_agent_list: () => [
BASE_BREADCRUMB,
{
href: pagePathGetters.fleet(),
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', {
defaultMessage: 'Fleet',
}),
},
{
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', {
text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', {
defaultMessage: 'Agents',
}),
},
@ -178,12 +172,7 @@ const breadcrumbGetters: {
BASE_BREADCRUMB,
{
href: pagePathGetters.fleet(),
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', {
defaultMessage: 'Fleet',
}),
},
{
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', {
text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', {
defaultMessage: 'Agents',
}),
},
@ -193,12 +182,12 @@ const breadcrumbGetters: {
BASE_BREADCRUMB,
{
href: pagePathGetters.fleet(),
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', {
defaultMessage: 'Fleet',
text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', {
defaultMessage: 'Agents',
}),
},
{
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle', {
text: i18n.translate('xpack.ingestManager.breadcrumbs.enrollmentTokensPageTitle', {
defaultMessage: 'Enrollment tokens',
}),
},

View file

@ -83,8 +83,8 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
disabled={!fleet?.enabled}
>
<FormattedMessage
id="xpack.ingestManager.appNavigation.fleetLinkText"
defaultMessage="Fleet"
id="xpack.ingestManager.appNavigation.agentsLinkText"
defaultMessage="Agents"
/>
</EuiTab>
<EuiTab isSelected={section === 'data_stream'} href={getHref('data_streams')}>

View file

@ -74,14 +74,14 @@ export const ManagedInstructions = React.memo<Props>(({ agentPolicies }) => {
) : (
<>
<FormattedMessage
id="xpack.ingestManager.agentEnrollment.fleetNotInitializedText"
id="xpack.ingestManager.agentEnrollment.agentsNotInitializedText"
defaultMessage="Before enrolling agents, {link}."
values={{
link: (
<EuiLink href={getHref('fleet')}>
<FormattedMessage
id="xpack.ingestManager.agentEnrollment.setUpFleetLink"
defaultMessage="set up Fleet"
id="xpack.ingestManager.agentEnrollment.setUpAgentsLink"
defaultMessage="set up Agents"
/>
</EuiLink>
),

View file

@ -126,7 +126,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {
<EuiFlexItem>
<EuiText>
<h1>
<FormattedMessage id="xpack.ingestManager.fleet.pageTitle" defaultMessage="Fleet" />
<FormattedMessage id="xpack.ingestManager.agents.pageTitle" defaultMessage="Agents" />
</h1>
</EuiText>
</EuiFlexItem>
@ -134,7 +134,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.ingestManager.fleet.pageSubtitle"
id="xpack.ingestManager.agents.pageSubtitle"
defaultMessage="Manage and deploy policy updates to a group of agents of any size."
/>
</p>

View file

@ -25,8 +25,8 @@ export const OverviewAgentSection = () => {
return (
<EuiFlexItem component="section">
<OverviewPanel
title={i18n.translate('xpack.ingestManager.overviewPageFleetPanelTitle', {
defaultMessage: 'Fleet',
title={i18n.translate('xpack.ingestManager.overviewPageAgentsPanelTitle', {
defaultMessage: 'Agents',
})}
tooltip={i18n.translate('xpack.ingestManager.overviewPageFleetPanelTooltip', {
defaultMessage:

View file

@ -47,7 +47,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => {
<h1>
<FormattedMessage
id="xpack.ingestManager.overviewPageTitle"
defaultMessage="Ingest Manager"
defaultMessage="Fleet"
/>
</h1>
</EuiTitle>

View file

@ -78,7 +78,7 @@ export class IngestManagerPlugin
core.application.register({
id: PLUGIN_ID,
category: DEFAULT_APP_CATEGORIES.management,
title: i18n.translate('xpack.ingestManager.appTitle', { defaultMessage: 'Ingest Manager' }),
title: i18n.translate('xpack.ingestManager.appTitle', { defaultMessage: 'Fleet' }),
order: 9020,
euiIconType: 'logoElastic',
async mount(params: AppMountParameters) {

View file

@ -9,7 +9,6 @@ import { SavedObjectsClientContract } from 'kibana/server';
import { saveInstalledEsRefs } from '../../packages/install';
import * as Registry from '../../registry';
import {
Dataset,
ElasticsearchAssetType,
EsAssetReference,
RegistryPackage,
@ -24,12 +23,7 @@ interface TransformInstallation {
content: string;
}
interface TransformPathDataset {
path: string;
dataset: Dataset;
}
export const installTransformForDataset = async (
export const installTransform = async (
registryPackage: RegistryPackage,
paths: string[],
callCluster: CallESAsCurrentUser,
@ -51,53 +45,32 @@ export const installTransformForDataset = async (
callCluster,
previousInstalledTransformEsAssets.map((asset) => asset.id)
);
// install the latest dataset
const datasets = registryPackage.datasets;
if (!datasets?.length) return [];
const installNameSuffix = `${registryPackage.version}`;
const installNameSuffix = `${registryPackage.version}`;
const transformPaths = paths.filter((path) => isTransform(path));
let installedTransforms: EsAssetReference[] = [];
if (transformPaths.length > 0) {
const transformPathDatasets = datasets.reduce<TransformPathDataset[]>((acc, dataset) => {
transformPaths.forEach((path) => {
if (isDatasetTransform(path, dataset.path)) {
acc.push({ path, dataset });
}
const transformRefs = transformPaths.reduce<EsAssetReference[]>((acc, path) => {
acc.push({
id: getTransformNameForInstallation(registryPackage, path, installNameSuffix),
type: ElasticsearchAssetType.transform,
});
return acc;
}, []);
const transformRefs = transformPathDatasets.reduce<EsAssetReference[]>(
(acc, transformPathDataset) => {
if (transformPathDataset) {
acc.push({
id: getTransformNameForInstallation(transformPathDataset, installNameSuffix),
type: ElasticsearchAssetType.transform,
});
}
return acc;
},
[]
);
// get and save transform refs before installing transforms
await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs);
const transforms: TransformInstallation[] = transformPathDatasets.map(
(transformPathDataset: TransformPathDataset) => {
return {
installationName: getTransformNameForInstallation(
transformPathDataset,
installNameSuffix
),
content: getAsset(transformPathDataset.path).toString('utf-8'),
};
}
);
const transforms: TransformInstallation[] = transformPaths.map((path: string) => {
return {
installationName: getTransformNameForInstallation(registryPackage, path, installNameSuffix),
content: getAsset(path).toString('utf-8'),
};
});
const installationPromises = transforms.map(async (transform) => {
return installTransform({ callCluster, transform });
return handleTransformInstall({ callCluster, transform });
});
installedTransforms = await Promise.all(installationPromises).then((results) => results.flat());
@ -123,20 +96,10 @@ export const installTransformForDataset = async (
const isTransform = (path: string) => {
const pathParts = Registry.pathParts(path);
return pathParts.type === ElasticsearchAssetType.transform;
return !path.endsWith('/') && pathParts.type === ElasticsearchAssetType.transform;
};
const isDatasetTransform = (path: string, datasetName: string) => {
const pathParts = Registry.pathParts(path);
return (
!path.endsWith('/') &&
pathParts.type === ElasticsearchAssetType.transform &&
pathParts.dataset !== undefined &&
datasetName === pathParts.dataset
);
};
async function installTransform({
async function handleTransformInstall({
callCluster,
transform,
}: {
@ -160,9 +123,12 @@ async function installTransform({
}
const getTransformNameForInstallation = (
transformDataset: TransformPathDataset,
registryPackage: RegistryPackage,
path: string,
suffix: string
) => {
const filename = transformDataset?.path.split('/')?.pop()?.split('.')[0];
return `${transformDataset.dataset.type}-${transformDataset.dataset.name}-${filename}-${suffix}`;
const pathPaths = path.split('/');
const filename = pathPaths?.pop()?.split('.')[0];
const folderName = pathPaths?.pop();
return `${registryPackage.name}.${folderName}-${filename}-${suffix}`;
};

View file

@ -25,6 +25,19 @@ export const deleteTransforms = async (
) => {
await Promise.all(
transformIds.map(async (transformId) => {
// get the index the transform
const transformResponse: {
count: number;
transforms: Array<{
dest: {
index: string;
};
}>;
} = await callCluster('transport.request', {
method: 'GET',
path: `/_transform/${transformId}`,
});
await stopTransforms([transformId], callCluster);
await callCluster('transport.request', {
method: 'DELETE',
@ -32,6 +45,15 @@ export const deleteTransforms = async (
path: `/_transform/${transformId}`,
ignore: [404],
});
// expect this to be 1
for (const transform of transformResponse.transforms) {
await callCluster('transport.request', {
method: 'DELETE',
path: `/${transform?.dest?.index}`,
ignore: [404],
});
}
})
);
};

View file

@ -14,7 +14,7 @@ jest.mock('./common', () => {
};
});
import { installTransformForDataset } from './install';
import { installTransform } from './install';
import { ILegacyScopedClusterClient, SavedObject, SavedObjectsClientContract } from 'kibana/server';
import { ElasticsearchAssetType, Installation, RegistryPackage } from '../../../../types';
import { getInstallation, getInstallationObject } from '../../packages';
@ -47,7 +47,7 @@ describe('test transform install', () => {
type: ElasticsearchAssetType.ingestPipeline,
},
{
id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
id: 'endpoint.metadata_current-default-0.15.0-dev.0',
type: ElasticsearchAssetType.transform,
},
],
@ -60,15 +60,15 @@ describe('test transform install', () => {
type: ElasticsearchAssetType.ingestPipeline,
},
{
id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
id: 'endpoint.metadata_current-default-0.15.0-dev.0',
type: ElasticsearchAssetType.transform,
},
{
id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
id: 'endpoint.metadata_current-default-0.16.0-dev.0',
type: ElasticsearchAssetType.transform,
},
{
id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
id: 'endpoint.metadata-default-0.16.0-dev.0',
type: ElasticsearchAssetType.transform,
},
],
@ -91,7 +91,26 @@ describe('test transform install', () => {
} as unknown) as SavedObject<Installation>)
);
await installTransformForDataset(
legacyScopedClusterClient.callAsCurrentUser.mockReturnValueOnce(
Promise.resolve({
count: 1,
transforms: [
{
dest: {
index: 'index',
},
},
],
} as {
count: number;
transforms: Array<{
dest: {
index: string;
};
}>;
})
);
await installTransform(
({
name: 'endpoint',
version: '0.16.0-dev.0',
@ -128,18 +147,26 @@ describe('test transform install', () => {
} as unknown) as RegistryPackage,
[
'endpoint-0.16.0-dev.0/dataset/policy/elasticsearch/ingest_pipeline/default.json',
'endpoint-0.16.0-dev.0/dataset/metadata/elasticsearch/transform/default.json',
'endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json',
'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata/default.json',
'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json',
],
legacyScopedClusterClient.callAsCurrentUser,
savedObjectsClient
);
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
[
'transport.request',
{
method: 'GET',
path: '/_transform/endpoint.metadata_current-default-0.15.0-dev.0',
},
],
[
'transport.request',
{
method: 'POST',
path: '/_transform/metrics-endpoint.metadata_current-default-0.15.0-dev.0/_stop',
path: '/_transform/endpoint.metadata_current-default-0.15.0-dev.0/_stop',
query: 'force=true',
ignore: [404],
},
@ -149,7 +176,15 @@ describe('test transform install', () => {
{
method: 'DELETE',
query: 'force=true',
path: '/_transform/metrics-endpoint.metadata_current-default-0.15.0-dev.0',
path: '/_transform/endpoint.metadata_current-default-0.15.0-dev.0',
ignore: [404],
},
],
[
'transport.request',
{
method: 'DELETE',
path: '/index',
ignore: [404],
},
],
@ -157,7 +192,7 @@ describe('test transform install', () => {
'transport.request',
{
method: 'PUT',
path: '/_transform/metrics-endpoint.metadata-default-0.16.0-dev.0',
path: '/_transform/endpoint.metadata-default-0.16.0-dev.0',
query: 'defer_validation=true',
body: '{"content": "data"}',
},
@ -166,7 +201,7 @@ describe('test transform install', () => {
'transport.request',
{
method: 'PUT',
path: '/_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0',
path: '/_transform/endpoint.metadata_current-default-0.16.0-dev.0',
query: 'defer_validation=true',
body: '{"content": "data"}',
},
@ -175,14 +210,14 @@ describe('test transform install', () => {
'transport.request',
{
method: 'POST',
path: '/_transform/metrics-endpoint.metadata-default-0.16.0-dev.0/_start',
path: '/_transform/endpoint.metadata-default-0.16.0-dev.0/_start',
},
],
[
'transport.request',
{
method: 'POST',
path: '/_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0/_start',
path: '/_transform/endpoint.metadata_current-default-0.16.0-dev.0/_start',
},
],
]);
@ -198,15 +233,15 @@ describe('test transform install', () => {
type: 'ingest_pipeline',
},
{
id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
id: 'endpoint.metadata_current-default-0.15.0-dev.0',
type: 'transform',
},
{
id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
id: 'endpoint.metadata-default-0.16.0-dev.0',
type: 'transform',
},
{
id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
id: 'endpoint.metadata_current-default-0.16.0-dev.0',
type: 'transform',
},
],
@ -222,11 +257,11 @@ describe('test transform install', () => {
type: 'ingest_pipeline',
},
{
id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
id: 'endpoint.metadata_current-default-0.16.0-dev.0',
type: 'transform',
},
{
id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
id: 'endpoint.metadata-default-0.16.0-dev.0',
type: 'transform',
},
],
@ -263,7 +298,7 @@ describe('test transform install', () => {
>)
);
legacyScopedClusterClient.callAsCurrentUser = jest.fn();
await installTransformForDataset(
await installTransform(
({
name: 'endpoint',
version: '0.16.0-dev.0',
@ -284,7 +319,7 @@ describe('test transform install', () => {
},
],
} as unknown) as RegistryPackage,
['endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json'],
['endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json'],
legacyScopedClusterClient.callAsCurrentUser,
savedObjectsClient
);
@ -294,7 +329,7 @@ describe('test transform install', () => {
'transport.request',
{
method: 'PUT',
path: '/_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0',
path: '/_transform/endpoint.metadata_current-default-0.16.0-dev.0',
query: 'defer_validation=true',
body: '{"content": "data"}',
},
@ -303,7 +338,7 @@ describe('test transform install', () => {
'transport.request',
{
method: 'POST',
path: '/_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0/_start',
path: '/_transform/endpoint.metadata_current-default-0.16.0-dev.0/_start',
},
],
]);
@ -313,7 +348,7 @@ describe('test transform install', () => {
'endpoint',
{
installed_es: [
{ id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' },
{ id: 'endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' },
],
},
],
@ -324,7 +359,7 @@ describe('test transform install', () => {
const previousInstallation: Installation = ({
installed_es: [
{
id: 'metrics-endpoint.metadata-current-default-0.15.0-dev.0',
id: 'endpoint.metadata-current-default-0.15.0-dev.0',
type: ElasticsearchAssetType.transform,
},
],
@ -346,7 +381,26 @@ describe('test transform install', () => {
} as unknown) as SavedObject<Installation>)
);
await installTransformForDataset(
legacyScopedClusterClient.callAsCurrentUser.mockReturnValueOnce(
Promise.resolve({
count: 1,
transforms: [
{
dest: {
index: 'index',
},
},
],
} as {
count: number;
transforms: Array<{
dest: {
index: string;
};
}>;
})
);
await installTransform(
({
name: 'endpoint',
version: '0.16.0-dev.0',
@ -387,11 +441,18 @@ describe('test transform install', () => {
);
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
[
'transport.request',
{
method: 'GET',
path: '/_transform/endpoint.metadata-current-default-0.15.0-dev.0',
},
],
[
'transport.request',
{
method: 'POST',
path: '/_transform/metrics-endpoint.metadata-current-default-0.15.0-dev.0/_stop',
path: '/_transform/endpoint.metadata-current-default-0.15.0-dev.0/_stop',
query: 'force=true',
ignore: [404],
},
@ -401,7 +462,15 @@ describe('test transform install', () => {
{
method: 'DELETE',
query: 'force=true',
path: '/_transform/metrics-endpoint.metadata-current-default-0.15.0-dev.0',
path: '/_transform/endpoint.metadata-current-default-0.15.0-dev.0',
ignore: [404],
},
],
[
'transport.request',
{
method: 'DELETE',
path: '/index',
ignore: [404],
},
],

View file

@ -21,7 +21,6 @@ import {
ElasticsearchAssetType,
InstallType,
} from '../../../types';
import { appContextService } from '../../index';
import { installIndexPatterns } from '../kibana/index_pattern/install';
import * as Registry from '../registry';
import {
@ -45,7 +44,8 @@ import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove';
import { IngestManagerError, PackageOutdatedError } from '../../../errors';
import { getPackageSavedObjects } from './get';
import { installTransformForDataset } from '../elasticsearch/transform/install';
import { installTransform } from '../elasticsearch/transform/install';
import { appContextService } from '../../app_context';
export async function installLatestPackage(options: {
savedObjectsClient: SavedObjectsClientContract;
@ -325,7 +325,7 @@ export async function installPackage({
// update current backing indices of each data stream
await updateCurrentWriteIndices(callCluster, installedTemplates);
const installedTransforms = await installTransformForDataset(
const installedTransforms = await installTransform(
registryPackageInfo,
paths,
callCluster,

View file

@ -179,6 +179,41 @@ const createActions = (testBed: TestBed<TestSubject>) => {
});
},
clickDocumentsDropdown() {
act(() => {
find('documentsDropdown.documentsButton').simulate('click');
});
component.update();
},
clickEditDocumentsButton() {
act(() => {
find('editDocumentsButton').simulate('click');
});
component.update();
},
clickClearAllButton() {
act(() => {
find('clearAllDocumentsButton').simulate('click');
});
component.update();
},
async clickConfirmResetButton() {
const modal = document.body.querySelector(
'[data-test-subj="resetDocumentsConfirmationModal"]'
);
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
'[data-test-subj="confirmModalConfirmButton"]'
);
await act(async () => {
confirmButton!.click();
});
component.update();
},
async clickProcessor(processorSelector: string) {
await act(async () => {
find(`${processorSelector}.manageItemButton`).simulate('click');
@ -230,6 +265,7 @@ type TestSubject =
| 'addDocumentsButton'
| 'testPipelineFlyout'
| 'documentsDropdown'
| 'documentsDropdown.documentsButton'
| 'outputTab'
| 'documentsEditor'
| 'runPipelineButton'
@ -248,6 +284,8 @@ type TestSubject =
| 'configurationTab'
| 'outputTab'
| 'processorOutputTabContent'
| 'editDocumentsButton'
| 'clearAllDocumentsButton'
| 'addDocumentsAccordion'
| 'addDocumentButton'
| 'addDocumentError'

View file

@ -22,6 +22,27 @@ describe('Test pipeline', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
// This is a hack
// We need to provide the processor id in the mocked output;
// this is generated dynamically
// As a workaround, the value is added as a data attribute in the UI
// and we retrieve it to generate the mocked output.
const addProcessorTagtoMockOutput = (output: VerboseTestOutput) => {
const { find } = testBed;
const docs = output.docs.map((doc) => {
const results = doc.processor_results.map((result, index) => {
const tag = find(`processors>${index}`).props()['data-processor-id'];
return {
...result,
tag,
};
});
return { processor_results: results };
});
return { docs };
};
beforeAll(() => {
jest.useFakeTimers();
});
@ -236,30 +257,77 @@ describe('Test pipeline', () => {
expect(find('addDocumentError').text()).toContain(error.message);
});
});
describe('Documents dropdown', () => {
beforeEach(async () => {
const { actions } = testBed;
httpRequestsMockHelpers.setSimulatePipelineResponse(
addProcessorTagtoMockOutput(SIMULATE_RESPONSE)
);
// Open flyout
actions.clickAddDocumentsButton();
// Add sample documents and click run
actions.addDocumentsJson(JSON.stringify(DOCUMENTS));
await actions.clickRunPipelineButton();
// Close flyout
actions.closeTestPipelineFlyout();
});
it('should open flyout to edit documents', () => {
const { exists, actions } = testBed;
// Dropdown should be visible
expect(exists('documentsDropdown')).toBe(true);
// Open dropdown and edit documents
actions.clickDocumentsDropdown();
actions.clickEditDocumentsButton();
// Flyout should be visible with "Documents" tab enabled
expect(exists('testPipelineFlyout')).toBe(true);
expect(exists('documentsTabContent')).toBe(true);
});
it('should clear all documents and stop pipeline simulation', async () => {
const { exists, actions, find } = testBed;
// Dropdown should be visible and processor status should equal "success"
expect(exists('documentsDropdown')).toBe(true);
const initialProcessorStatusLabel = find('processors>0.processorStatusIcon').props()[
'aria-label'
];
expect(initialProcessorStatusLabel).toEqual('Success');
// Open flyout and click clear all button
actions.clickDocumentsDropdown();
actions.clickEditDocumentsButton();
actions.clickClearAllButton();
// Verify modal
const modal = document.body.querySelector(
'[data-test-subj="resetDocumentsConfirmationModal"]'
);
expect(modal).not.toBe(null);
expect(modal!.textContent).toContain('Clear documents');
// Confirm reset and close modal
await actions.clickConfirmResetButton();
// Verify documents and processors were reset
expect(exists('documentsDropdown')).toBe(false);
expect(exists('addDocumentsButton')).toBe(true);
const resetProcessorStatusIconLabel = find('processors>0.processorStatusIcon').props()[
'aria-label'
];
expect(resetProcessorStatusIconLabel).toEqual('Not run');
});
});
});
describe('Processors', () => {
// This is a hack
// We need to provide the processor id in the mocked output;
// this is generated dynamically and not something we can stub.
// As a workaround, the value is added as a data attribute in the UI
// and we retrieve it to generate the mocked output.
const addProcessorTagtoMockOutput = (output: VerboseTestOutput) => {
const { find } = testBed;
const docs = output.docs.map((doc) => {
const results = doc.processor_results.map((result, index) => {
const tag = find(`processors>${index}`).props()['data-processor-id'];
return {
...result,
tag,
};
});
return { processor_results: results };
});
return { docs };
};
it('should show "inactive" processor status by default', async () => {
const { find } = testBed;

View file

@ -102,7 +102,7 @@ export const EditProcessorForm: FunctionComponent<Props> = ({
handleSubmit,
resetProcessors,
}) => {
const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext();
const { testPipelineData, testPipelineDataDispatch } = useTestPipelineContext();
const {
testOutputPerProcessor,
config: { selectedDocumentIndex, documents },
@ -117,7 +117,7 @@ export const EditProcessorForm: FunctionComponent<Props> = ({
testOutputPerProcessor[selectedDocumentIndex][processor.id];
const updateSelectedDocument = (index: number) => {
setCurrentTestPipelineData({
testPipelineDataDispatch({
type: 'updateActiveDocument',
payload: {
config: {

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs';
import { TestPipelineFlyoutTab } from './test_pipeline_tabs';
const i18nTexts = {
buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel', {

View file

@ -8,18 +8,15 @@ import React, { FunctionComponent, useState } from 'react';
import {
EuiButton,
EuiPopover,
EuiPopoverFooter,
EuiButtonEmpty,
EuiPopoverTitle,
EuiSelectable,
EuiHorizontalRule,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';
import { Document } from '../../../types';
import { TestPipelineFlyoutTab } from '../test_pipeline_flyout_tabs';
import { TestPipelineFlyoutTab } from '../test_pipeline_tabs';
import './documents_dropdown.scss';
@ -31,9 +28,9 @@ const i18nTexts = {
}
),
addDocumentsButtonLabel: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.buttonLabel',
'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.editDocumentsButtonLabel',
{
defaultMessage: 'Add documents',
defaultMessage: 'Edit documents',
}
),
popoverTitle: i18n.translate(
@ -88,8 +85,10 @@ export const DocumentsDropdown: FunctionComponent<Props> = ({
>
<EuiSelectable
singleSelection
data-test-subj="documentList"
options={documents.map((doc, index) => ({
key: index.toString(),
'data-test-subj': 'documentListItem',
checked: selectedDocumentIndex === index ? 'on' : undefined,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.documentLabel', {
defaultMessage: 'Document {documentNumber}',
@ -107,32 +106,27 @@ export const DocumentsDropdown: FunctionComponent<Props> = ({
setShowPopover(false);
}}
>
{(list, search) => (
<div>
{(list) => (
<>
<EuiPopoverTitle>{i18nTexts.popoverTitle}</EuiPopoverTitle>
{list}
</div>
</>
)}
</EuiSelectable>
<EuiHorizontalRule margin="xs" />
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
size="s"
onClick={() => {
openFlyout('documents');
setShowPopover(false);
}}
data-test-subj="addDocumentsButton"
>
{i18nTexts.addDocumentsButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiPopoverFooter>
<EuiButton
size="s"
fullWidth
onClick={() => {
openFlyout('documents');
setShowPopover(false);
}}
data-test-subj="editDocumentsButton"
>
{i18nTexts.addDocumentsButtonLabel}
</EuiButton>
</EuiPopoverFooter>
</EuiPopover>
);
};

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import { EuiButton } from '@elastic/eui';
import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs';
import { TestPipelineFlyoutTab } from './test_pipeline_tabs';
const i18nTexts = {
buttonLabel: i18n.translate(

View file

@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { useTestPipelineContext, usePipelineProcessorsContext } from '../../context';
import { DocumentsDropdown } from './documents_dropdown';
import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs';
import { TestPipelineFlyoutTab } from './test_pipeline_tabs';
import { AddDocumentsButton } from './add_documents_button';
import { TestOutputButton } from './test_output_button';
import { TestPipelineFlyout } from './test_pipeline_flyout.container';
@ -24,7 +24,7 @@ const i18nTexts = {
};
export const TestPipelineActions: FunctionComponent = () => {
const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext();
const { testPipelineData, testPipelineDataDispatch } = useTestPipelineContext();
const {
state: { processors },
@ -39,7 +39,7 @@ export const TestPipelineActions: FunctionComponent = () => {
const [activeFlyoutTab, setActiveFlyoutTab] = useState<TestPipelineFlyoutTab>('documents');
const updateSelectedDocument = (index: number) => {
setCurrentTestPipelineData({
testPipelineDataDispatch({
type: 'updateActiveDocument',
payload: {
config: {

View file

@ -15,8 +15,7 @@ import { Document } from '../../types';
import { useIsMounted } from '../../use_is_mounted';
import { TestPipelineFlyout as ViewComponent } from './test_pipeline_flyout';
import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs';
import { documentsSchema } from './test_pipeline_flyout_tabs/documents_schema';
import { TestPipelineFlyoutTab } from './test_pipeline_tabs';
export interface Props {
activeTab: TestPipelineFlyoutTab;
@ -39,7 +38,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
const {
testPipelineData,
setCurrentTestPipelineData,
testPipelineDataDispatch,
updateTestOutputPerProcessor,
} = useTestPipelineContext();
@ -48,7 +47,6 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
} = testPipelineData;
const { form } = useForm({
schema: documentsSchema,
defaultValue: {
documents: cachedDocuments || '',
},
@ -88,7 +86,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
// reset the per-processor output
// this is needed in the scenario where the pipeline has already executed,
// but you modified the sample documents and there was an error on re-execution
setCurrentTestPipelineData({
testPipelineDataDispatch({
type: 'updateOutputPerProcessor',
payload: {
isExecutingPipeline: false,
@ -99,7 +97,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
return { isSuccessful: false };
}
setCurrentTestPipelineData({
testPipelineDataDispatch({
type: 'updateConfig',
payload: {
config: {
@ -133,7 +131,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
processors,
services.api,
services.notifications.toasts,
setCurrentTestPipelineData,
testPipelineDataDispatch,
updateTestOutputPerProcessor,
]
);
@ -157,6 +155,12 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
}
};
const resetTestOutput = () => {
testPipelineDataDispatch({
type: 'reset',
});
};
useEffect(() => {
if (cachedDocuments && activeTab === 'output') {
handleTestPipeline({ documents: cachedDocuments, verbose: cachedVerbose }, true);
@ -169,6 +173,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
return (
<ViewComponent
handleTestPipeline={handleTestPipeline}
resetTestOutput={resetTestOutput}
isRunningTest={isRunningTest}
cachedVerbose={cachedVerbose}
cachedDocuments={cachedDocuments}

View file

@ -19,8 +19,7 @@ import {
import { FormHook } from '../../../../../shared_imports';
import { Document } from '../../types';
import { Tabs, TestPipelineFlyoutTab, OutputTab, DocumentsTab } from './test_pipeline_flyout_tabs';
import { Tabs, TestPipelineFlyoutTab, OutputTab, DocumentsTab } from './test_pipeline_tabs';
export interface Props {
onClose: () => void;
handleTestPipeline: (
@ -31,11 +30,14 @@ export interface Props {
cachedVerbose?: boolean;
cachedDocuments?: Document[];
testOutput?: any;
form: FormHook;
form: FormHook<{
documents: string | Document[];
}>;
validateAndTestPipeline: () => Promise<void>;
selectedTab: TestPipelineFlyoutTab;
setSelectedTab: (selectedTa: TestPipelineFlyoutTab) => void;
testingError: any;
resetTestOutput: () => void;
}
export interface TestPipelineConfig {
@ -45,6 +47,7 @@ export interface TestPipelineConfig {
export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
handleTestPipeline,
resetTestOutput,
isRunningTest,
cachedVerbose,
cachedDocuments,
@ -75,6 +78,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
form={form}
validateAndTestPipeline={validateAndTestPipeline}
isRunningTest={isRunningTest}
resetTestOutput={resetTestOutput}
/>
);
}

View file

@ -1,111 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiCode } from '@elastic/eui';
import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../../shared_imports';
import { parseJson, stringifyJson } from '../../../../../lib';
const { emptyField, isJsonField } = fieldValidators;
export const documentsSchema: FormSchema = {
documents: {
label: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel',
{
defaultMessage: 'Documents',
}
),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.form.onFailureFieldHelpText"
defaultMessage="Use JSON format: {code}"
values={{
code: (
<EuiCode>
{JSON.stringify([
{
_index: 'index',
_id: 'id',
_source: {
foo: 'bar',
},
},
])}
</EuiCode>
),
}}
/>
),
serializer: parseJson,
deserializer: stringifyJson,
validations: [
{
validator: emptyField(
i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError',
{
defaultMessage: 'Documents are required.',
}
)
),
},
{
validator: isJsonField(
i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError',
{
defaultMessage: 'The documents JSON is not valid.',
}
)
),
},
{
validator: ({ value }: ValidationFuncArg<any, any>) => {
const parsedJSON = JSON.parse(value);
if (!parsedJSON.length) {
return {
message: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError',
{
defaultMessage: 'At least one document is required.',
}
),
};
}
},
},
{
validator: ({ value }: ValidationFuncArg<any, any>) => {
const parsedJSON = JSON.parse(value);
const isMissingSourceField = parsedJSON.find((document: { _source?: object }) => {
if (!document._source) {
return true;
}
return false;
});
if (isMissingSourceField) {
return {
message: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsForm.sourceFieldRequiredError',
{
defaultMessage: 'Documents require a _source field.',
}
),
};
}
},
},
],
},
};

View file

@ -1,134 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiText, EuiButton, EuiLink } from '@elastic/eui';
import {
getUseField,
Field,
JsonEditorField,
useKibana,
useFormData,
FormHook,
Form,
} from '../../../../../../shared_imports';
import { AddDocumentsAccordion } from './add_documents_accordion';
const UseField = getUseField({ component: Field });
interface Props {
validateAndTestPipeline: () => Promise<void>;
isRunningTest: boolean;
form: FormHook;
}
export const DocumentsTab: FunctionComponent<Props> = ({
validateAndTestPipeline,
isRunningTest,
form,
}) => {
const { services } = useKibana();
const [, formatData] = useFormData({ form });
const onAddDocumentHandler = useCallback(
(document) => {
const { documents: existingDocuments = [] } = formatData();
form.reset({ defaultValue: { documents: [...existingDocuments, document] } });
},
[form, formatData]
);
return (
<Form
form={form}
data-test-subj="testPipelineForm"
isInvalid={form.isSubmitted && !form.isValid}
onSubmit={validateAndTestPipeline}
error={form.getErrors()}
>
<div data-test-subj="documentsTabContent">
<EuiText>
<p>
<FormattedMessage
id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText"
defaultMessage="Provide documents for the pipeline to ingest. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink
href={`${services.documentation.getEsDocsBasePath()}/simulate-pipeline-api.html`}
target="_blank"
external
>
{i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink',
{
defaultMessage: 'Learn more.',
}
)}
</EuiLink>
),
}}
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<AddDocumentsAccordion onAddDocuments={onAddDocumentHandler} />
<EuiSpacer size="l" />
{/* Documents editor */}
<UseField
path="documents"
component={JsonEditorField}
componentProps={{
euiCodeEditorProps: {
'data-test-subj': 'documentsEditor',
height: '300px',
'aria-label': i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldAriaLabel',
{
defaultMessage: 'Documents JSON editor',
}
),
},
}}
/>
<EuiSpacer size="m" />
<EuiButton
onClick={validateAndTestPipeline}
data-test-subj="runPipelineButton"
size="s"
isLoading={isRunningTest}
disabled={form.isSubmitted && !form.isValid}
iconType="play"
>
{isRunningTest ? (
<FormattedMessage
id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.runningButtonLabel"
defaultMessage="Running"
/>
) : (
<FormattedMessage
id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.runButtonLabel"
defaultMessage="Run the pipeline"
/>
)}
</EuiButton>
</div>
</Form>
);
};

View file

@ -25,9 +25,9 @@ import {
TextField,
fieldValidators,
FieldConfig,
} from '../../../../../../shared_imports';
import { useIsMounted } from '../../../use_is_mounted';
import { Document } from '../../../types';
} from '../../../../../../../shared_imports';
import { useIsMounted } from '../../../../use_is_mounted';
import { Document } from '../../../../types';
const UseField = getUseField({ component: Field });

View file

@ -11,8 +11,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiAccordion, EuiText, EuiSpacer, EuiLink } from '@elastic/eui';
import { UrlGeneratorsDefinition } from 'src/plugins/share/public';
import { useKibana } from '../../../../../../../shared_imports';
import { useIsMounted } from '../../../../use_is_mounted';
import { useKibana } from '../../../../../../../../shared_imports';
import { useIsMounted } from '../../../../../use_is_mounted';
import { AddDocumentForm } from '../add_document_form';
import './add_documents_accordion.scss';
@ -26,6 +26,12 @@ const i18nTexts = {
defaultMessage: 'Add documents from index',
}
),
addDocumentsDescription: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.contentDescriptionText',
{
defaultMessage: 'Provide the index name and document ID of the indexed document to test.',
}
),
};
interface Props {
@ -79,10 +85,7 @@ export const AddDocumentsAccordion: FunctionComponent<Props> = ({ onAddDocuments
<div className="addDocumentsAccordion">
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.contentDescriptionText"
defaultMessage="Provide the index name and document ID of the indexed document to test."
/>
{i18nTexts.addDocumentsDescription}
{discoverLink && (
<>
{' '}

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { DocumentsTab } from './tab_documents';

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
interface Props {
confirmResetTestOutput: () => void;
closeModal: () => void;
}
const i18nTexts = {
modalTitle: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.title',
{
defaultMessage: 'Clear documents',
}
),
modalDescription: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.description',
{
defaultMessage: 'This will stop pipeline simulation.',
}
),
cancelButtonLabel: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.cancelButtonLabel',
{
defaultMessage: 'Cancel',
}
),
resetButtonLabel: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.testPipeline.resetDocumentsModal.resetButtonLabel',
{
defaultMessage: 'Clear documents',
}
),
};
export const ResetDocumentsModal: FunctionComponent<Props> = ({
confirmResetTestOutput,
closeModal,
}) => {
return (
<EuiOverlayMask>
<EuiConfirmModal
buttonColor="danger"
data-test-subj="resetDocumentsConfirmationModal"
title={i18nTexts.modalTitle}
onCancel={closeModal}
onConfirm={confirmResetTestOutput}
cancelButtonText={i18nTexts.cancelButtonLabel}
confirmButtonText={i18nTexts.resetButtonLabel}
>
<p>{i18nTexts.modalDescription}</p>
</EuiConfirmModal>
</EuiOverlayMask>
);
};

View file

@ -0,0 +1,11 @@
.documentsTab {
&__documentField {
position: relative;
&__button {
position: absolute;
right: 0;
top: 0;
}
}
}

View file

@ -0,0 +1,251 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent, useCallback, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiText, EuiButton, EuiLink, EuiCode, EuiButtonEmpty } from '@elastic/eui';
import { parseJson, stringifyJson } from '../../../../../../lib';
import {
getUseField,
Field,
JsonEditorField,
useKibana,
FieldConfig,
fieldValidators,
ValidationFuncArg,
FormHook,
Form,
useFormData,
} from '../../../../../../../shared_imports';
import { Document } from '../../../../types';
import { AddDocumentsAccordion } from './add_documents_accordion';
import { ResetDocumentsModal } from './reset_documents_modal';
import './tab_documents.scss';
const UseField = getUseField({ component: Field });
const { emptyField, isJsonField } = fieldValidators;
interface Props {
validateAndTestPipeline: () => Promise<void>;
resetTestOutput: () => void;
isRunningTest: boolean;
form: FormHook<{
documents: string | Document[];
}>;
}
const i18nTexts = {
learnMoreLink: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink',
{
defaultMessage: 'Learn more.',
}
),
documentsEditorAriaLabel: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldAriaLabel',
{
defaultMessage: 'Documents JSON editor',
}
),
documentsEditorClearAllButton: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldClearAllButtonLabel',
{
defaultMessage: 'Clear all',
}
),
runButton: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsTab.runButtonLabel',
{
defaultMessage: 'Run the pipeline',
}
),
runningButton: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsTab.runningButtonLabel',
{
defaultMessage: 'Running',
}
),
};
const documentFieldConfig: FieldConfig = {
label: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel',
{
defaultMessage: 'Documents',
}
),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.form.onFailureFieldHelpText"
defaultMessage="Use JSON format: {code}"
values={{
code: (
<EuiCode>
{JSON.stringify([
{
_index: 'index',
_id: 'id',
_source: {
foo: 'bar',
},
},
])}
</EuiCode>
),
}}
/>
),
serializer: parseJson,
deserializer: stringifyJson,
validations: [
{
validator: emptyField(
i18n.translate('xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError', {
defaultMessage: 'Documents are required.',
})
),
},
{
validator: isJsonField(
i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError',
{
defaultMessage: 'The documents JSON is not valid.',
}
)
),
},
{
validator: ({ value }: ValidationFuncArg<any, any>) => {
const parsedJSON = JSON.parse(value);
if (!parsedJSON.length) {
return {
message: i18n.translate(
'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError',
{
defaultMessage: 'At least one document is required.',
}
),
};
}
},
},
],
};
export const DocumentsTab: FunctionComponent<Props> = ({
validateAndTestPipeline,
isRunningTest,
form,
resetTestOutput,
}) => {
const { services } = useKibana();
const [, formatData] = useFormData({ form });
const onAddDocumentHandler = useCallback(
(document) => {
const { documents: existingDocuments = [] } = formatData();
form.reset({ defaultValue: { documents: [...existingDocuments, document] } });
},
[form, formatData]
);
const [showResetModal, setShowResetModal] = useState<boolean>(false);
return (
<Form
form={form}
data-test-subj="testPipelineForm"
isInvalid={form.isSubmitted && !form.isValid}
onSubmit={validateAndTestPipeline}
error={form.getErrors()}
>
<div data-test-subj="documentsTabContent" className="documentsTab">
<EuiText>
<p>
<FormattedMessage
id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText"
defaultMessage="Provide documents for the pipeline to ingest. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink
href={`${services.documentation.getEsDocsBasePath()}/simulate-pipeline-api.html`}
target="_blank"
external
>
{i18nTexts.learnMoreLink}
</EuiLink>
),
}}
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<AddDocumentsAccordion onAddDocuments={onAddDocumentHandler} />
<EuiSpacer size="l" />
{/* Documents editor */}
<UseField config={documentFieldConfig} path="documents">
{(field) => (
<div className="documentsTab__documentField">
<EuiButtonEmpty
size="xs"
onClick={() => setShowResetModal(true)}
data-test-subj="clearAllDocumentsButton"
className="documentsTab__documentField__button"
>
{i18nTexts.documentsEditorClearAllButton}
</EuiButtonEmpty>
<JsonEditorField
field={field}
euiCodeEditorProps={{
'data-test-subj': 'documentsEditor',
height: '300px',
'aria-label': i18nTexts.documentsEditorAriaLabel,
}}
/>
</div>
)}
</UseField>
<EuiSpacer size="m" />
<EuiButton
onClick={validateAndTestPipeline}
data-test-subj="runPipelineButton"
size="s"
isLoading={isRunningTest}
disabled={form.isSubmitted && !form.isValid}
iconType="play"
>
{isRunningTest ? i18nTexts.runningButton : i18nTexts.runButton}
</EuiButton>
{showResetModal && (
<ResetDocumentsModal
confirmResetTestOutput={() => {
resetTestOutput();
form.reset({ defaultValue: { documents: [] } });
setShowResetModal(false);
}}
closeModal={() => setShowResetModal(false)}
/>
)}
</div>
</Form>
);
};

View file

@ -51,11 +51,14 @@ type Action =
| {
type: 'updateIsExecutingPipeline';
payload: Pick<TestPipelineData, 'isExecutingPipeline'>;
}
| {
type: 'reset';
};
export interface TestPipelineContext {
testPipelineData: TestPipelineData;
setCurrentTestPipelineData: (data: Action) => void;
testPipelineDataDispatch: (data: Action) => void;
updateTestOutputPerProcessor: (
documents: Document[] | undefined,
processors: DeserializeResult
@ -69,7 +72,7 @@ const DEFAULT_TEST_PIPELINE_CONTEXT = {
},
isExecutingPipeline: false,
},
setCurrentTestPipelineData: () => {},
testPipelineDataDispatch: () => {},
updateTestOutputPerProcessor: () => {},
};
@ -122,6 +125,10 @@ export const reducer: Reducer<TestPipelineData, Action> = (state, action) => {
};
}
if (action.type === 'reset') {
return DEFAULT_TEST_PIPELINE_CONTEXT.testPipelineData;
}
return state;
};
@ -193,7 +200,7 @@ export const TestPipelineContextProvider = ({ children }: { children: React.Reac
<TestPipelineContext.Provider
value={{
testPipelineData: state,
setCurrentTestPipelineData: dispatch,
testPipelineDataDispatch: dispatch,
updateTestOutputPerProcessor,
}}
>

View file

@ -7,8 +7,8 @@
export const eventsIndexPattern = 'logs-endpoint.events.*';
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current-*';
export const metadataTransformPrefix = 'metrics-endpoint.metadata-current-default';
export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current_*';
export const metadataTransformPrefix = 'endpoint.metadata_current-default';
export const policyIndexPattern = 'metrics-endpoint.policy-*';
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';

View file

@ -158,6 +158,7 @@ describe('Alerts', () => {
});
});
});
context('Opening alerts', () => {
beforeEach(() => {
esArchiverLoad('closed_alerts');
@ -204,6 +205,7 @@ describe('Alerts', () => {
});
});
});
context('Marking alerts as in-progress', () => {
beforeEach(() => {
esArchiverLoad('alerts');

View file

@ -43,6 +43,7 @@ describe('Alerts detection rules', () => {
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded();
cy.get(RULE_NAME)
.eq(FIFTH_RULE)
.invoke('text')
@ -56,7 +57,6 @@ describe('Alerts detection rules', () => {
activateRule(SEVENTH_RULE);
waitForRuleToBeActivated();
sortByActivatedRules();
cy.get(RULE_NAME)
.eq(FIRST_RULE)
.invoke('text')
@ -70,7 +70,6 @@ describe('Alerts detection rules', () => {
cy.wrap(expectedRulesNames).should('include', seventhRuleName);
});
});
cy.get(RULE_SWITCH).eq(FIRST_RULE).should('have.attr', 'role', 'switch');
cy.get(RULE_SWITCH).eq(SECOND_RULE).should('have.attr', 'role', 'switch');
});

View file

@ -4,7 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { newRule, existingRule } from '../objects/rule';
import { newRule, existingRule, indexPatterns, editedRule } from '../objects/rule';
import {
ALERT_RULE_METHOD,
ALERT_RULE_NAME,
ALERT_RULE_RISK_SCORE,
ALERT_RULE_SEVERITY,
ALERT_RULE_VERSION,
NUMBER_OF_ALERTS,
} from '../screens/alerts';
import {
CUSTOM_RULES_BTN,
@ -12,20 +20,49 @@ import {
RULE_NAME,
RULES_ROW,
RULES_TABLE,
RULE_SWITCH,
SEVERITY,
SHOWING_RULES_TEXT,
} from '../screens/alerts_detection_rules';
import {
ABOUT_CONTINUE_BTN,
ABOUT_EDIT_BUTTON,
ACTIONS_THROTTLE_INPUT,
CUSTOM_QUERY_INPUT,
DEFINE_CONTINUE_BUTTON,
DEFINE_EDIT_BUTTON,
DEFINE_INDEX_INPUT,
RISK_INPUT,
RULE_DESCRIPTION_INPUT,
RULE_NAME_INPUT,
SCHEDULE_INTERVAL_AMOUNT_INPUT,
SCHEDULE_INTERVAL_UNITS_INPUT,
SEVERITY_DROPDOWN,
TAGS_FIELD,
} from '../screens/create_new_rule';
import {
ADDITIONAL_LOOK_BACK_DETAILS,
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
getDetails,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
RULE_NAME_HEADER,
getDescriptionForTitle,
ABOUT_DETAILS,
DEFINITION_DETAILS,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
} from '../screens/rule_details';
import {
@ -37,46 +74,46 @@ import {
changeToThreeHundredRowsPerPage,
deleteFirstRule,
deleteSelectedRules,
editFirstRule,
filterByCustomRules,
goToCreateNewRule,
goToRuleDetails,
selectNumberOfRules,
waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded,
waitForRulesToBeLoaded,
editFirstRule,
} from '../tasks/alerts_detection_rules';
import {
createAndActivateRule,
fillAboutRule,
fillAboutRuleAndContinue,
fillDefineCustomRuleWithImportedQueryAndContinue,
fillScheduleRuleAndContinue,
goToAboutStepTab,
goToScheduleStepTab,
goToActionsStepTab,
fillAboutRule,
goToScheduleStepTab,
waitForTheRuleToBeExecuted,
} from '../tasks/create_new_rule';
import { saveEditedRule } from '../tasks/edit_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { refreshPage } from '../tasks/security_header';
import { DETECTIONS_URL } from '../urls/navigation';
import {
ACTIONS_THROTTLE_INPUT,
CUSTOM_QUERY_INPUT,
DEFINE_INDEX_INPUT,
RULE_NAME_INPUT,
RULE_DESCRIPTION_INPUT,
TAGS_FIELD,
SEVERITY_DROPDOWN,
RISK_INPUT,
SCHEDULE_INTERVAL_AMOUNT_INPUT,
SCHEDULE_INTERVAL_UNITS_INPUT,
DEFINE_EDIT_BUTTON,
DEFINE_CONTINUE_BUTTON,
ABOUT_EDIT_BUTTON,
ABOUT_CONTINUE_BTN,
} from '../screens/create_new_rule';
import { saveEditedRule } from '../tasks/edit_rule';
describe('Detection rules, custom', () => {
const expectedUrls = newRule.referenceUrls.join('');
const expectedFalsePositives = newRule.falsePositivesExamples.join('');
const expectedTags = newRule.tags.join('');
const expectedMitre = newRule.mitre
.map(function (mitre) {
return mitre.tactic + mitre.techniques.join('');
})
.join('');
const expectedNumberOfRules = 1;
const expectedEditedtags = editedRule.tags.join('');
const expectedEditedIndexPatterns =
editedRule.index && editedRule.index.length ? editedRule.index : indexPatterns;
describe('Custom detection rules creation', () => {
before(() => {
esArchiverLoad('timeline');
});
@ -85,7 +122,7 @@ describe('Detection rules, custom', () => {
esArchiverUnload('timeline');
});
it('Creates and activates a new custom rule', () => {
it('Creates and activates a new rule', () => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
@ -94,27 +131,27 @@ describe('Detection rules, custom', () => {
goToCreateNewRule();
fillDefineCustomRuleWithImportedQueryAndContinue(newRule);
fillAboutRuleAndContinue(newRule);
fillScheduleRuleAndContinue(newRule);
// expect define step to repopulate
cy.get(DEFINE_EDIT_BUTTON).click();
cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', newRule.customQuery);
cy.get(CUSTOM_QUERY_INPUT).should('have.text', newRule.customQuery);
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true });
cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist');
// expect about step to populate
cy.get(ABOUT_EDIT_BUTTON).click();
cy.get(RULE_NAME_INPUT).invoke('val').should('eq', newRule.name);
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', newRule.name);
cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true });
cy.get(ABOUT_CONTINUE_BTN).should('not.exist');
createAndActivateRule();
cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
changeToThreeHundredRowsPerPage();
waitForRulesToBeLoaded();
const expectedNumberOfRules = 1;
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
});
@ -124,78 +161,59 @@ describe('Detection rules, custom', () => {
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
});
cy.get(RULE_NAME).invoke('text').should('eql', newRule.name);
cy.get(RISK_SCORE).invoke('text').should('eql', newRule.riskScore);
cy.get(SEVERITY).invoke('text').should('eql', newRule.severity);
cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true');
cy.get(RULE_NAME).should('have.text', newRule.name);
cy.get(RISK_SCORE).should('have.text', newRule.riskScore);
cy.get(SEVERITY).should('have.text', newRule.severity);
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetails();
let expectedUrls = '';
newRule.referenceUrls.forEach((url) => {
expectedUrls = expectedUrls + url;
});
let expectedFalsePositives = '';
newRule.falsePositivesExamples.forEach((falsePositive) => {
expectedFalsePositives = expectedFalsePositives + falsePositive;
});
let expectedTags = '';
newRule.tags.forEach((tag) => {
expectedTags = expectedTags + tag;
});
let expectedMitre = '';
newRule.mitre.forEach((mitre) => {
expectedMitre = expectedMitre + mitre.tactic;
mitre.techniques.forEach((technique) => {
expectedMitre = expectedMitre + technique;
});
});
const expectedIndexPatterns = [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
];
cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newRule.description);
cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newRule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDescriptionForTitle('Severity').invoke('text').should('eql', newRule.severity);
getDescriptionForTitle('Risk score').invoke('text').should('eql', newRule.riskScore);
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
getDescriptionForTitle('False positive examples')
.invoke('text')
.should('eql', expectedFalsePositives);
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
getDetails(SEVERITY_DETAILS).should('have.text', newRule.severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', newRule.riskScore);
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN);
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDescriptionForTitle('Index patterns')
.invoke('text')
.should('eql', expectedIndexPatterns.join(''));
getDescriptionForTitle('Custom query')
.invoke('text')
.should('eql', `${newRule.customQuery} `);
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query');
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newRule.customQuery} `);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should(
'have.text',
`${newRule.runsEvery.interval}${newRule.runsEvery.type}`
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
'have.text',
`${newRule.lookBack.interval}${newRule.lookBack.type}`
);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
});
refreshPage();
waitForTheRuleToBeExecuted();
cy.get(NUMBER_OF_ALERTS)
.invoke('text')
.then((numberOfAlertsText) => {
cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.above', 0);
});
cy.get(ALERT_RULE_NAME).first().should('have.text', newRule.name);
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query');
cy.get(ALERT_RULE_SEVERITY).first().should('have.text', newRule.severity.toLowerCase());
cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newRule.riskScore);
});
});
describe('Deletes custom rules', () => {
describe('Custom detection rules deletion and edition', () => {
beforeEach(() => {
esArchiverLoad('custom_rules');
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
@ -208,143 +226,132 @@ describe('Deletes custom rules', () => {
esArchiverUnload('custom_rules');
});
it('Deletes one rule', () => {
cy.get(RULES_TABLE)
.find(RULES_ROW)
.then((rules) => {
const initialNumberOfRules = rules.length;
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1;
context('Deletion', () => {
it('Deletes one rule', () => {
cy.get(RULES_TABLE)
.find(RULES_ROW)
.then((rules) => {
const initialNumberOfRules = rules.length;
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1;
cy.get(SHOWING_RULES_TEXT)
.invoke('text')
.should('eql', `Showing ${initialNumberOfRules} rules`);
cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${initialNumberOfRules} rules`);
deleteFirstRule();
waitForRulesToBeLoaded();
deleteFirstRule();
waitForRulesToBeLoaded();
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should(
'eql',
expectedNumberOfRulesAfterDeletion
);
});
cy.get(SHOWING_RULES_TEXT).should(
'have.text',
`Showing ${expectedNumberOfRulesAfterDeletion} rules`
);
cy.get(CUSTOM_RULES_BTN).should(
'have.text',
`Custom rules (${expectedNumberOfRulesAfterDeletion})`
);
});
cy.get(SHOWING_RULES_TEXT)
.invoke('text')
.should('eql', `Showing ${expectedNumberOfRulesAfterDeletion} rules`);
cy.get(CUSTOM_RULES_BTN)
.invoke('text')
.should('eql', `Custom rules (${expectedNumberOfRulesAfterDeletion})`);
});
});
it('Deletes more than one rule', () => {
cy.get(RULES_TABLE)
.find(RULES_ROW)
.then((rules) => {
const initialNumberOfRules = rules.length;
const numberOfRulesToBeDeleted = 3;
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - numberOfRulesToBeDeleted;
selectNumberOfRules(numberOfRulesToBeDeleted);
deleteSelectedRules();
waitForRulesToBeLoaded();
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
});
cy.get(SHOWING_RULES_TEXT)
.invoke('text')
.should('eql', `Showing ${expectedNumberOfRulesAfterDeletion} rule`);
cy.get(CUSTOM_RULES_BTN)
.invoke('text')
.should('eql', `Custom rules (${expectedNumberOfRulesAfterDeletion})`);
});
});
it('Allows a rule to be edited', () => {
editFirstRule();
// expect define step to populate
cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', existingRule.customQuery);
if (existingRule.index && existingRule.index.length > 0) {
cy.get(DEFINE_INDEX_INPUT).invoke('text').should('eq', existingRule.index.join(''));
}
goToAboutStepTab();
// expect about step to populate
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name);
cy.get(RULE_DESCRIPTION_INPUT).invoke('text').should('eql', existingRule.description);
cy.get(TAGS_FIELD).invoke('text').should('eql', existingRule.tags.join(''));
cy.get(SEVERITY_DROPDOWN).invoke('text').should('eql', existingRule.severity);
cy.get(RISK_INPUT).invoke('val').should('eql', existingRule.riskScore);
goToScheduleStepTab();
// expect schedule step to populate
const intervalParts = existingRule.interval && existingRule.interval.match(/[0-9]+|[a-zA-Z]+/g);
if (intervalParts) {
const [amount, unit] = intervalParts;
cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount);
cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', unit);
} else {
throw new Error('Cannot assert scheduling info on a rule without an interval');
}
goToActionsStepTab();
cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions');
goToAboutStepTab();
const editedRule = {
...existingRule,
severity: 'Medium',
description: 'Edited Rule description',
};
fillAboutRule(editedRule);
saveEditedRule();
const expectedTags = editedRule.tags.join('');
const expectedIndexPatterns =
editedRule.index && editedRule.index.length
? editedRule.index
: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
];
cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${editedRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', editedRule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDescriptionForTitle('Severity').invoke('text').should('eql', editedRule.severity);
getDescriptionForTitle('Risk score').invoke('text').should('eql', editedRule.riskScore);
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
});
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', editedRule.note);
it('Deletes more than one rule', () => {
cy.get(RULES_TABLE)
.find(RULES_ROW)
.then((rules) => {
const initialNumberOfRules = rules.length;
const numberOfRulesToBeDeleted = 3;
const expectedNumberOfRulesAfterDeletion =
initialNumberOfRules - numberOfRulesToBeDeleted;
cy.get(DEFINITION_DETAILS).within(() => {
getDescriptionForTitle('Index patterns')
.invoke('text')
.should('eql', expectedIndexPatterns.join(''));
getDescriptionForTitle('Custom query')
.invoke('text')
.should('eql', `${editedRule.customQuery} `);
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query');
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
selectNumberOfRules(numberOfRulesToBeDeleted);
deleteSelectedRules();
waitForRulesToBeLoaded();
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should(
'eql',
expectedNumberOfRulesAfterDeletion
);
});
cy.get(SHOWING_RULES_TEXT).should(
'have.text',
`Showing ${expectedNumberOfRulesAfterDeletion} rule`
);
cy.get(CUSTOM_RULES_BTN).should(
'have.text',
`Custom rules (${expectedNumberOfRulesAfterDeletion})`
);
});
});
});
if (editedRule.interval) {
cy.get(SCHEDULE_DETAILS).within(() => {
getDescriptionForTitle('Runs every').invoke('text').should('eql', editedRule.interval);
context('Edition', () => {
it('Allows a rule to be edited', () => {
editFirstRule();
// expect define step to populate
cy.get(CUSTOM_QUERY_INPUT).should('have.text', existingRule.customQuery);
if (existingRule.index && existingRule.index.length > 0) {
cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.index.join(''));
}
goToAboutStepTab();
// expect about step to populate
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name);
cy.get(RULE_DESCRIPTION_INPUT).should('have.text', existingRule.description);
cy.get(TAGS_FIELD).should('have.text', existingRule.tags.join(''));
cy.get(SEVERITY_DROPDOWN).should('have.text', existingRule.severity);
cy.get(RISK_INPUT).invoke('val').should('eql', existingRule.riskScore);
goToScheduleStepTab();
// expect schedule step to populate
const intervalParts =
existingRule.interval && existingRule.interval.match(/[0-9]+|[a-zA-Z]+/g);
if (intervalParts) {
const [amount, unit] = intervalParts;
cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount);
cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', unit);
} else {
throw new Error('Cannot assert scheduling info on a rule without an interval');
}
goToActionsStepTab();
cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions');
goToAboutStepTab();
fillAboutRule(editedRule);
saveEditedRule();
cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', editedRule.severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', editedRule.riskScore);
getDetails(TAGS_DETAILS).should('have.text', expectedEditedtags);
});
}
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE)
.eq(INVESTIGATION_NOTES_TOGGLE)
.click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', editedRule.note);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should(
'have.text',
expectedEditedIndexPatterns.join('')
);
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${editedRule.customQuery} `);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
if (editedRule.interval) {
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should('have.text', editedRule.interval);
});
}
});
});
});

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { eqlRule } from '../objects/rule';
import { eqlRule, indexPatterns } from '../objects/rule';
import {
CUSTOM_RULES_BTN,
@ -12,19 +12,32 @@ import {
RULE_NAME,
RULES_ROW,
RULES_TABLE,
RULE_SWITCH,
SEVERITY,
} from '../screens/alerts_detection_rules';
import {
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
getDescriptionForTitle,
FALSE_POSITIVES_DETAILS,
getDetails,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
} from '../screens/rule_details';
import {
@ -43,14 +56,25 @@ import {
import {
createAndActivateRule,
fillAboutRuleAndContinue,
selectEqlRuleType,
fillDefineEqlRuleAndContinue,
fillScheduleRuleAndContinue,
selectEqlRuleType,
} from '../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { DETECTIONS_URL } from '../urls/navigation';
const expectedUrls = eqlRule.referenceUrls.join('');
const expectedFalsePositives = eqlRule.falsePositivesExamples.join('');
const expectedTags = eqlRule.tags.join('');
const expectedMitre = eqlRule.mitre
.map(function (mitre) {
return mitre.tactic + mitre.techniques.join('');
})
.join('');
const expectedNumberOfRules = 1;
describe('Detection rules, EQL', () => {
before(() => {
esArchiverLoad('timeline');
@ -70,14 +94,14 @@ describe('Detection rules, EQL', () => {
selectEqlRuleType();
fillDefineEqlRuleAndContinue(eqlRule);
fillAboutRuleAndContinue(eqlRule);
fillScheduleRuleAndContinue(eqlRule);
createAndActivateRule();
cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
changeToThreeHundredRowsPerPage();
waitForRulesToBeLoaded();
const expectedNumberOfRules = 1;
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
});
@ -87,73 +111,40 @@ describe('Detection rules, EQL', () => {
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
});
cy.get(RULE_NAME).invoke('text').should('eql', eqlRule.name);
cy.get(RISK_SCORE).invoke('text').should('eql', eqlRule.riskScore);
cy.get(SEVERITY).invoke('text').should('eql', eqlRule.severity);
cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true');
cy.get(RULE_NAME).should('have.text', eqlRule.name);
cy.get(RISK_SCORE).should('have.text', eqlRule.riskScore);
cy.get(SEVERITY).should('have.text', eqlRule.severity);
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetails();
let expectedUrls = '';
eqlRule.referenceUrls.forEach((url) => {
expectedUrls = expectedUrls + url;
});
let expectedFalsePositives = '';
eqlRule.falsePositivesExamples.forEach((falsePositive) => {
expectedFalsePositives = expectedFalsePositives + falsePositive;
});
let expectedTags = '';
eqlRule.tags.forEach((tag) => {
expectedTags = expectedTags + tag;
});
let expectedMitre = '';
eqlRule.mitre.forEach((mitre) => {
expectedMitre = expectedMitre + mitre.tactic;
mitre.techniques.forEach((technique) => {
expectedMitre = expectedMitre + technique;
});
});
const expectedIndexPatterns = [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
];
cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${eqlRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', eqlRule.description);
cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', eqlRule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDescriptionForTitle('Severity').invoke('text').should('eql', eqlRule.severity);
getDescriptionForTitle('Risk score').invoke('text').should('eql', eqlRule.riskScore);
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
getDescriptionForTitle('False positive examples')
.invoke('text')
.should('eql', expectedFalsePositives);
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
getDetails(SEVERITY_DETAILS).should('have.text', eqlRule.severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', eqlRule.riskScore);
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN);
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDescriptionForTitle('Index patterns')
.invoke('text')
.should('eql', expectedIndexPatterns.join(''));
getDescriptionForTitle('Custom query')
.invoke('text')
.should('eql', `${eqlRule.customQuery} `);
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Event Correlation');
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${eqlRule.customQuery} `);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
getDetails(RUNS_EVERY_DETAILS).should(
'have.text',
`${eqlRule.runsEvery.interval}${eqlRule.runsEvery.type}`
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
'have.text',
`${eqlRule.lookBack.interval}${eqlRule.lookBack.type}`
);
});
});
});

View file

@ -16,14 +16,25 @@ import {
SEVERITY,
} from '../screens/alerts_detection_rules';
import {
ABOUT_DETAILS,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
ANOMALY_SCORE_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
getDetails,
MACHINE_LEARNING_JOB_ID,
MACHINE_LEARNING_JOB_STATUS,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
getDescriptionForTitle,
ABOUT_DETAILS,
DEFINITION_DETAILS,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
} from '../screens/rule_details';
import {
@ -43,6 +54,7 @@ import {
createAndActivateRule,
fillAboutRuleAndContinue,
fillDefineMachineLearningRuleAndContinue,
fillScheduleRuleAndContinue,
selectMachineLearningRuleType,
} from '../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
@ -50,6 +62,16 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { DETECTIONS_URL } from '../urls/navigation';
const expectedUrls = machineLearningRule.referenceUrls.join('');
const expectedFalsePositives = machineLearningRule.falsePositivesExamples.join('');
const expectedTags = machineLearningRule.tags.join('');
const expectedMitre = machineLearningRule.mitre
.map(function (mitre) {
return mitre.tactic + mitre.techniques.join('');
})
.join('');
const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1;
describe('Detection rules, machine learning', () => {
before(() => {
esArchiverLoad('prebuilt_rules_loaded');
@ -69,6 +91,7 @@ describe('Detection rules, machine learning', () => {
selectMachineLearningRuleType();
fillDefineMachineLearningRuleAndContinue(machineLearningRule);
fillAboutRuleAndContinue(machineLearningRule);
fillScheduleRuleAndContinue(machineLearningRule);
createAndActivateRule();
cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');
@ -76,7 +99,6 @@ describe('Detection rules, machine learning', () => {
changeToThreeHundredRowsPerPage();
waitForRulesToBeLoaded();
const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1;
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
});
@ -86,67 +108,42 @@ describe('Detection rules, machine learning', () => {
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
});
cy.get(RULE_NAME).invoke('text').should('eql', machineLearningRule.name);
cy.get(RISK_SCORE).invoke('text').should('eql', machineLearningRule.riskScore);
cy.get(SEVERITY).invoke('text').should('eql', machineLearningRule.severity);
cy.get(RULE_NAME).should('have.text', machineLearningRule.name);
cy.get(RISK_SCORE).should('have.text', machineLearningRule.riskScore);
cy.get(SEVERITY).should('have.text', machineLearningRule.severity);
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetails();
let expectedUrls = '';
machineLearningRule.referenceUrls.forEach((url) => {
expectedUrls = expectedUrls + url;
});
let expectedFalsePositives = '';
machineLearningRule.falsePositivesExamples.forEach((falsePositive) => {
expectedFalsePositives = expectedFalsePositives + falsePositive;
});
let expectedTags = '';
machineLearningRule.tags.forEach((tag) => {
expectedTags = expectedTags + tag;
});
let expectedMitre = '';
machineLearningRule.mitre.forEach((mitre) => {
expectedMitre = expectedMitre + mitre.tactic;
mitre.techniques.forEach((technique) => {
expectedMitre = expectedMitre + technique;
});
});
cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${machineLearningRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', machineLearningRule.description);
cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', machineLearningRule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDescriptionForTitle('Severity').invoke('text').should('eql', machineLearningRule.severity);
getDescriptionForTitle('Risk score')
.invoke('text')
.should('eql', machineLearningRule.riskScore);
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
getDescriptionForTitle('False positive examples')
.invoke('text')
.should('eql', expectedFalsePositives);
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', machineLearningRule.riskScore);
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(DEFINITION_DETAILS).within(() => {
getDescriptionForTitle('Anomaly score')
.invoke('text')
.should('eql', machineLearningRule.anomalyScoreThreshold);
getDescriptionForTitle('Anomaly score')
.invoke('text')
.should('eql', machineLearningRule.anomalyScoreThreshold);
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Machine Learning');
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
cy.get(MACHINE_LEARNING_JOB_STATUS).invoke('text').should('eql', 'Stopped');
cy.get(MACHINE_LEARNING_JOB_ID)
.invoke('text')
.should('eql', machineLearningRule.machineLearningJob);
getDetails(ANOMALY_SCORE_DETAILS).should(
'have.text',
machineLearningRule.anomalyScoreThreshold
);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Machine Learning');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
cy.get(MACHINE_LEARNING_JOB_STATUS).should('have.text', 'Stopped');
cy.get(MACHINE_LEARNING_JOB_ID).should('have.text', machineLearningRule.machineLearningJob);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
getDetails(RUNS_EVERY_DETAILS).should(
'have.text',
`${machineLearningRule.runsEvery.interval}${machineLearningRule.runsEvery.type}`
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
'have.text',
`${machineLearningRule.lookBack.interval}${machineLearningRule.lookBack.type}`
);
});
});
});

View file

@ -4,33 +4,58 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { newOverrideRule } from '../objects/rule';
import { indexPatterns, newOverrideRule, severitiesOverride } from '../objects/rule';
import {
NUMBER_OF_ALERTS,
ALERT_RULE_NAME,
ALERT_RULE_METHOD,
ALERT_RULE_RISK_SCORE,
ALERT_RULE_SEVERITY,
ALERT_RULE_VERSION,
} from '../screens/alerts';
import {
CUSTOM_RULES_BTN,
RISK_SCORE,
RULE_NAME,
RULE_SWITCH,
RULES_ROW,
RULES_TABLE,
SEVERITY,
} from '../screens/alerts_detection_rules';
import {
ABOUT_INVESTIGATION_NOTES,
ABOUT_DETAILS,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
DETAILS_DESCRIPTION,
DETAILS_TITLE,
FALSE_POSITIVES_DETAILS,
getDetails,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RISK_SCORE_OVERRIDE_DETAILS,
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
RULE_NAME_HEADER,
ABOUT_DETAILS,
getDescriptionForTitle,
DEFINITION_DETAILS,
RULE_NAME_OVERRIDE_DETAILS,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
DETAILS_TITLE,
DETAILS_DESCRIPTION,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
TIMESTAMP_OVERRIDE_DETAILS,
} from '../screens/rule_details';
import {
goToManageAlertsDetectionRules,
sortRiskScore,
waitForAlertsIndexToBeCreated,
waitForAlertsPanelToBeLoaded,
} from '../tasks/alerts';
@ -46,12 +71,24 @@ import {
createAndActivateRule,
fillAboutRuleWithOverrideAndContinue,
fillDefineCustomRuleWithImportedQueryAndContinue,
fillScheduleRuleAndContinue,
waitForTheRuleToBeExecuted,
} from '../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { refreshPage } from '../tasks/security_header';
import { DETECTIONS_URL } from '../urls/navigation';
const expectedUrls = newOverrideRule.referenceUrls.join('');
const expectedFalsePositives = newOverrideRule.falsePositivesExamples.join('');
const expectedTags = newOverrideRule.tags.join('');
const expectedMitre = newOverrideRule.mitre
.map(function (mitre) {
return mitre.tactic + mitre.techniques.join('');
})
.join('');
describe('Detection rules, override', () => {
before(() => {
esArchiverLoad('timeline');
@ -70,9 +107,10 @@ describe('Detection rules, override', () => {
goToCreateNewRule();
fillDefineCustomRuleWithImportedQueryAndContinue(newOverrideRule);
fillAboutRuleWithOverrideAndContinue(newOverrideRule);
fillScheduleRuleAndContinue(newOverrideRule);
createAndActivateRule();
cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
changeToThreeHundredRowsPerPage();
waitForRulesToBeLoaded();
@ -87,98 +125,75 @@ describe('Detection rules, override', () => {
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
});
cy.get(RULE_NAME).invoke('text').should('eql', newOverrideRule.name);
cy.get(RISK_SCORE).invoke('text').should('eql', newOverrideRule.riskScore);
cy.get(SEVERITY).invoke('text').should('eql', newOverrideRule.severity);
cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true');
cy.get(RULE_NAME).should('have.text', newOverrideRule.name);
cy.get(RISK_SCORE).should('have.text', newOverrideRule.riskScore);
cy.get(SEVERITY).should('have.text', newOverrideRule.severity);
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetails();
let expectedUrls = '';
newOverrideRule.referenceUrls.forEach((url) => {
expectedUrls = expectedUrls + url;
});
let expectedFalsePositives = '';
newOverrideRule.falsePositivesExamples.forEach((falsePositive) => {
expectedFalsePositives = expectedFalsePositives + falsePositive;
});
let expectedTags = '';
newOverrideRule.tags.forEach((tag) => {
expectedTags = expectedTags + tag;
});
let expectedMitre = '';
newOverrideRule.mitre.forEach((mitre) => {
expectedMitre = expectedMitre + mitre.tactic;
mitre.techniques.forEach((technique) => {
expectedMitre = expectedMitre + technique;
});
});
const expectedIndexPatterns = [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
];
cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newOverrideRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newOverrideRule.description);
const expectedOverrideSeverities = ['Low', 'Medium', 'High', 'Critical'];
cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newOverrideRule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDescriptionForTitle('Severity').invoke('text').should('eql', newOverrideRule.severity);
getDescriptionForTitle('Risk score').invoke('text').should('eql', newOverrideRule.riskScore);
getDescriptionForTitle('Risk score override')
.invoke('text')
.should('eql', `${newOverrideRule.riskOverride}signal.rule.risk_score`);
getDescriptionForTitle('Rule name override')
.invoke('text')
.should('eql', newOverrideRule.nameOverride);
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
getDescriptionForTitle('False positive examples')
.invoke('text')
.should('eql', expectedFalsePositives);
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
getDescriptionForTitle('Timestamp override')
.invoke('text')
.should('eql', newOverrideRule.timestampOverride);
getDetails(SEVERITY_DETAILS).should('have.text', newOverrideRule.severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', newOverrideRule.riskScore);
getDetails(RISK_SCORE_OVERRIDE_DETAILS).should(
'have.text',
`${newOverrideRule.riskOverride}signal.rule.risk_score`
);
getDetails(RULE_NAME_OVERRIDE_DETAILS).should('have.text', newOverrideRule.nameOverride);
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
getDetails(TIMESTAMP_OVERRIDE_DETAILS).should('have.text', newOverrideRule.timestampOverride);
cy.contains(DETAILS_TITLE, 'Severity override')
.invoke('index', DETAILS_TITLE) // get index relative to other titles, not all siblings
.then((severityOverrideIndex) => {
newOverrideRule.severityOverride.forEach((severity, i) => {
cy.get(DETAILS_DESCRIPTION)
.eq(severityOverrideIndex + i)
.invoke('text')
.should(
'eql',
`${severity.sourceField}:${severity.sourceValue}${expectedOverrideSeverities[i]}`
'have.text',
`${severity.sourceField}:${severity.sourceValue}${severitiesOverride[i]}`
);
});
});
});
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN);
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDescriptionForTitle('Index patterns')
.invoke('text')
.should('eql', expectedIndexPatterns.join(''));
getDescriptionForTitle('Custom query')
.invoke('text')
.should('eql', `${newOverrideRule.customQuery} `);
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query');
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newOverrideRule.customQuery} `);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should(
'have.text',
`${newOverrideRule.runsEvery.interval}${newOverrideRule.runsEvery.type}`
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
'have.text',
`${newOverrideRule.lookBack.interval}${newOverrideRule.lookBack.type}`
);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
});
refreshPage();
waitForTheRuleToBeExecuted();
cy.get(NUMBER_OF_ALERTS)
.invoke('text')
.then((numberOfAlertsText) => {
cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.above', 0);
});
cy.get(ALERT_RULE_NAME).first().should('have.text', 'auditbeat');
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query');
cy.get(ALERT_RULE_SEVERITY).first().should('have.text', 'critical');
sortRiskScore();
cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', '80');
});
});

View file

@ -56,7 +56,7 @@ describe('Alerts rules, prebuilt rules', () => {
loadPrebuiltDetectionRules();
waitForPrebuiltDetectionRulesToBeLoaded();
cy.get(ELASTIC_RULES_BTN).invoke('text').should('eql', expectedElasticRulesBtnText);
cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText);
changeToThreeHundredRowsPerPage();
waitForRulesToBeLoaded();
@ -81,7 +81,7 @@ describe('Deleting prebuilt rules', () => {
loadPrebuiltDetectionRules();
waitForPrebuiltDetectionRulesToBeLoaded();
cy.get(ELASTIC_RULES_BTN).invoke('text').should('eql', expectedElasticRulesBtnText);
cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText);
changeToThreeHundredRowsPerPage();
waitForRulesToBeLoaded();
@ -113,16 +113,15 @@ describe('Deleting prebuilt rules', () => {
changeToThreeHundredRowsPerPage();
waitForRulesToBeLoaded();
cy.get(ELASTIC_RULES_BTN)
.invoke('text')
.should('eql', `Elastic rules (${expectedNumberOfRulesAfterDeletion})`);
cy.get(ELASTIC_RULES_BTN).should(
'have.text',
`Elastic rules (${expectedNumberOfRulesAfterDeletion})`
);
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
});
cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist');
cy.get(RELOAD_PREBUILT_RULES_BTN)
.invoke('text')
.should('eql', 'Install 1 Elastic prebuilt rule ');
cy.get(RELOAD_PREBUILT_RULES_BTN).should('have.text', 'Install 1 Elastic prebuilt rule ');
reloadDeletedRules();
@ -135,9 +134,10 @@ describe('Deleting prebuilt rules', () => {
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering);
});
cy.get(ELASTIC_RULES_BTN)
.invoke('text')
.should('eql', `Elastic rules (${expectedNumberOfRulesAfterRecovering})`);
cy.get(ELASTIC_RULES_BTN).should(
'have.text',
`Elastic rules (${expectedNumberOfRulesAfterRecovering})`
);
});
it('Deletes and recovers more than one rule', () => {
@ -152,12 +152,14 @@ describe('Deleting prebuilt rules', () => {
waitForRulesToBeLoaded();
cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist');
cy.get(RELOAD_PREBUILT_RULES_BTN)
.invoke('text')
.should('eql', `Install ${numberOfRulesToBeSelected} Elastic prebuilt rules `);
cy.get(ELASTIC_RULES_BTN)
.invoke('text')
.should('eql', `Elastic rules (${expectedNumberOfRulesAfterDeletion})`);
cy.get(RELOAD_PREBUILT_RULES_BTN).should(
'have.text',
`Install ${numberOfRulesToBeSelected} Elastic prebuilt rules `
);
cy.get(ELASTIC_RULES_BTN).should(
'have.text',
`Elastic rules (${expectedNumberOfRulesAfterDeletion})`
);
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
});
@ -173,8 +175,9 @@ describe('Deleting prebuilt rules', () => {
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering);
});
cy.get(ELASTIC_RULES_BTN)
.invoke('text')
.should('eql', `Elastic rules (${expectedNumberOfRulesAfterRecovering})`);
cy.get(ELASTIC_RULES_BTN).should(
'have.text',
`Elastic rules (${expectedNumberOfRulesAfterRecovering})`
);
});
});

View file

@ -4,27 +4,49 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { newThresholdRule } from '../objects/rule';
import { indexPatterns, newThresholdRule } from '../objects/rule';
import {
ALERT_RULE_METHOD,
ALERT_RULE_NAME,
ALERT_RULE_RISK_SCORE,
ALERT_RULE_SEVERITY,
ALERT_RULE_VERSION,
NUMBER_OF_ALERTS,
} from '../screens/alerts';
import {
CUSTOM_RULES_BTN,
RISK_SCORE,
RULE_NAME,
RULE_SWITCH,
RULES_ROW,
RULES_TABLE,
SEVERITY,
} from '../screens/alerts_detection_rules';
import {
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
CUSTOM_QUERY_DETAILS,
FALSE_POSITIVES_DETAILS,
DEFINITION_DETAILS,
getDetails,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
RULE_NAME_HEADER,
getDescriptionForTitle,
ABOUT_DETAILS,
DEFINITION_DETAILS,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
THRESHOLD_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
} from '../screens/rule_details';
import {
@ -44,13 +66,25 @@ import {
createAndActivateRule,
fillAboutRuleAndContinue,
fillDefineThresholdRuleAndContinue,
fillScheduleRuleAndContinue,
selectThresholdRuleType,
waitForTheRuleToBeExecuted,
} from '../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { refreshPage } from '../tasks/security_header';
import { DETECTIONS_URL } from '../urls/navigation';
const expectedUrls = newThresholdRule.referenceUrls.join('');
const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join('');
const expectedTags = newThresholdRule.tags.join('');
const expectedMitre = newThresholdRule.mitre
.map(function (mitre) {
return mitre.tactic + mitre.techniques.join('');
})
.join('');
describe('Detection rules, threshold', () => {
before(() => {
esArchiverLoad('timeline');
@ -70,9 +104,10 @@ describe('Detection rules, threshold', () => {
selectThresholdRuleType();
fillDefineThresholdRuleAndContinue(newThresholdRule);
fillAboutRuleAndContinue(newThresholdRule);
fillScheduleRuleAndContinue(newThresholdRule);
createAndActivateRule();
cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
changeToThreeHundredRowsPerPage();
waitForRulesToBeLoaded();
@ -87,79 +122,60 @@ describe('Detection rules, threshold', () => {
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
});
cy.get(RULE_NAME).invoke('text').should('eql', newThresholdRule.name);
cy.get(RISK_SCORE).invoke('text').should('eql', newThresholdRule.riskScore);
cy.get(SEVERITY).invoke('text').should('eql', newThresholdRule.severity);
cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true');
cy.get(RULE_NAME).should('have.text', newThresholdRule.name);
cy.get(RISK_SCORE).should('have.text', newThresholdRule.riskScore);
cy.get(SEVERITY).should('have.text', newThresholdRule.severity);
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetails();
let expectedUrls = '';
newThresholdRule.referenceUrls.forEach((url) => {
expectedUrls = expectedUrls + url;
});
let expectedFalsePositives = '';
newThresholdRule.falsePositivesExamples.forEach((falsePositive) => {
expectedFalsePositives = expectedFalsePositives + falsePositive;
});
let expectedTags = '';
newThresholdRule.tags.forEach((tag) => {
expectedTags = expectedTags + tag;
});
let expectedMitre = '';
newThresholdRule.mitre.forEach((mitre) => {
expectedMitre = expectedMitre + mitre.tactic;
mitre.techniques.forEach((technique) => {
expectedMitre = expectedMitre + technique;
});
});
const expectedIndexPatterns = [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
];
cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newThresholdRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newThresholdRule.description);
cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name} Beta`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThresholdRule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDescriptionForTitle('Severity').invoke('text').should('eql', newThresholdRule.severity);
getDescriptionForTitle('Risk score').invoke('text').should('eql', newThresholdRule.riskScore);
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
getDescriptionForTitle('False positive examples')
.invoke('text')
.should('eql', expectedFalsePositives);
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
getDetails(SEVERITY_DETAILS).should('have.text', newThresholdRule.severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', newThresholdRule.riskScore);
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN);
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDescriptionForTitle('Index patterns')
.invoke('text')
.should('eql', expectedIndexPatterns.join(''));
getDescriptionForTitle('Custom query')
.invoke('text')
.should('eql', `${newThresholdRule.customQuery} `);
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Threshold');
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
getDescriptionForTitle('Threshold')
.invoke('text')
.should(
'eql',
`Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}`
);
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newThresholdRule.customQuery} `);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
getDetails(THRESHOLD_DETAILS).should(
'have.text',
`Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}`
);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should(
'have.text',
`${newThresholdRule.runsEvery.interval}${newThresholdRule.runsEvery.type}`
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
'have.text',
`${newThresholdRule.lookBack.interval}${newThresholdRule.lookBack.type}`
);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
});
refreshPage();
waitForTheRuleToBeExecuted();
cy.get(NUMBER_OF_ALERTS)
.invoke('text')
.then((numberOfAlertsText) => {
cy.wrap(parseInt(numberOfAlertsText, 10)).should('be.below', 100);
});
cy.get(ALERT_RULE_NAME).first().should('have.text', newThresholdRule.name);
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold');
cy.get(ALERT_RULE_SEVERITY)
.first()
.should('have.text', newThresholdRule.severity.toLowerCase());
cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThresholdRule.riskScore);
});
});

View file

@ -35,7 +35,7 @@ describe('Alerts timeline', () => {
.invoke('text')
.then((eventId) => {
investigateFirstAlertInTimeline();
cy.get(PROVIDER_BADGE).invoke('text').should('eql', `_id: "${eventId}"`);
cy.get(PROVIDER_BADGE).should('have.text', `_id: "${eventId}"`);
});
});
});

View file

@ -21,8 +21,7 @@ import {
import { HOSTS_URL, NETWORK_URL } from '../urls/navigation';
// FLAKY: https://github.com/elastic/kibana/issues/78496
describe.skip('Inspect', () => {
describe('Inspect', () => {
context('Hosts stats and tables', () => {
before(() => {
loginAndWaitForPage(HOSTS_URL);

View file

@ -23,6 +23,12 @@ interface SeverityOverride {
sourceValue: string;
}
interface Interval {
interval: string;
timeType: string;
type: string;
}
export interface CustomRule {
customQuery: string;
name: string;
@ -38,6 +44,8 @@ export interface CustomRule {
mitre: Mitre[];
note: string;
timelineId: string;
runsEvery: Interval;
lookBack: Interval;
}
export interface ThresholdRule extends CustomRule {
@ -65,6 +73,8 @@ export interface MachineLearningRule {
falsePositivesExamples: string[];
mitre: Mitre[];
note: string;
runsEvery: Interval;
lookBack: Interval;
}
const mitre1: Mitre = {
@ -83,8 +93,8 @@ const severityOverride1: SeverityOverride = {
};
const severityOverride2: SeverityOverride = {
sourceField: 'agent.type',
sourceValue: 'endpoint',
sourceField: '@timestamp',
sourceValue: '10/02/2020',
};
const severityOverride3: SeverityOverride = {
@ -93,8 +103,20 @@ const severityOverride3: SeverityOverride = {
};
const severityOverride4: SeverityOverride = {
sourceField: '@timestamp',
sourceValue: '10/02/2020',
sourceField: 'agent.type',
sourceValue: 'auditbeat',
};
const runsEvery: Interval = {
interval: '1',
timeType: 'Seconds',
type: 's',
};
const lookBack: Interval = {
interval: '17520',
timeType: 'Hours',
type: 'h',
};
export const newRule: CustomRule = {
@ -109,6 +131,8 @@ export const newRule: CustomRule = {
mitre: [mitre1, mitre2],
note: '# test markdown',
timelineId: '0162c130-78be-11ea-9718-118a926974a4',
runsEvery,
lookBack,
};
export const existingRule: CustomRule = {
@ -132,6 +156,8 @@ export const existingRule: CustomRule = {
mitre: [],
note: 'This is my note',
timelineId: '',
runsEvery,
lookBack,
};
export const newOverrideRule: OverrideRule = {
@ -150,6 +176,8 @@ export const newOverrideRule: OverrideRule = {
riskOverride: 'destination.port',
nameOverride: 'agent.type',
timestampOverride: '@timestamp',
runsEvery,
lookBack,
};
export const newThresholdRule: ThresholdRule = {
@ -166,6 +194,8 @@ export const newThresholdRule: ThresholdRule = {
timelineId: '0162c130-78be-11ea-9718-118a926974a4',
thresholdField: 'host.name',
threshold: '10',
runsEvery,
lookBack,
};
export const machineLearningRule: MachineLearningRule = {
@ -180,6 +210,8 @@ export const machineLearningRule: MachineLearningRule = {
falsePositivesExamples: ['False1'],
mitre: [mitre1],
note: '# test markdown',
runsEvery,
lookBack,
};
export const eqlRule: CustomRule = {
@ -194,4 +226,24 @@ export const eqlRule: CustomRule = {
mitre: [mitre1, mitre2],
note: '# test markdown',
timelineId: '0162c130-78be-11ea-9718-118a926974a4',
runsEvery,
lookBack,
};
export const indexPatterns = [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'winlogbeat-*',
];
export const severitiesOverride = ['Low', 'Medium', 'High', 'Critical'];
export const editedRule = {
...existingRule,
severity: 'Medium',
description: 'Edited Rule description',
};

View file

@ -4,45 +4,57 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const ALERTS = '[data-test-subj="event"]';
export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input';
export const ALERT_ID = '[data-test-subj="draggable-content-_id"]';
export const ALERT_RISK_SCORE_HEADER = '[data-test-subj="header-text-signal.rule.risk_score"]';
export const ALERT_RULE_METHOD = '[data-test-subj="draggable-content-signal.rule.type"]';
export const ALERT_RULE_NAME = '[data-test-subj="draggable-content-signal.rule.name"]';
export const ALERT_RULE_RISK_SCORE = '[data-test-subj="draggable-content-signal.rule.risk_score"]';
export const ALERT_RULE_SEVERITY = '[data-test-subj="draggable-content-signal.rule.severity"]';
export const ALERT_RULE_VERSION = '[data-test-subj="draggable-content-signal.rule.version"]';
export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]';
export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closeSelectedAlertsButton"]';
export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]';
export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]';
export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]';
export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]';
export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]';
export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-status"]';
export const MARK_SELECTED_ALERTS_IN_PROGRESS_BTN =
'[data-test-subj="markSelectedAlertsInProgressButton"]';
export const NUMBER_OF_ALERTS = '[data-test-subj="server-side-event-count"] .euiBadge__text';
export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]';
export const OPEN_SELECTED_ALERTS_BTN = '[data-test-subj="openSelectedAlertsButton"]';
export const OPENED_ALERTS_FILTER_BTN = '[data-test-subj="openAlerts"]';
export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]';
export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]';
export const SELECTED_ALERTS = '[data-test-subj="selectedAlerts"]';
export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]';
export const SHOWING_ALERTS = '[data-test-subj="showingAlerts"]';
export const ALERTS = '[data-test-subj="event"]';
export const ALERT_ID = '[data-test-subj="draggable-content-_id"]';
export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input';
export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="alertActionPopover"] button';
export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]';
export const OPEN_SELECTED_ALERTS_BTN = '[data-test-subj="openSelectedAlertsButton"]';
export const CLOSE_SELECTED_ALERTS_BTN = '[data-test-subj="closeSelectedAlertsButton"]';
export const MARK_SELECTED_ALERTS_IN_PROGRESS_BTN =
'[data-test-subj="markSelectedAlertsInProgressButton"]';
export const OPEN_ALERT_BTN = '[data-test-subj="open-alert-status"]';
export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]';
export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-status"]';

View file

@ -57,6 +57,12 @@ export const INVESTIGATION_NOTES_TEXTAREA =
export const FALSE_POSITIVES_INPUT =
'[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] input';
export const LOOK_BACK_INTERVAL =
'[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="interval"]';
export const LOOK_BACK_TIME_TYPE =
'[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="timeType"]';
export const MACHINE_LEARNING_DROPDOWN = '[data-test-subj="mlJobSelect"] button';
export const MACHINE_LEARNING_LIST = '.euiContextMenuItem__text';
@ -73,6 +79,8 @@ export const MITRE_TECHNIQUES_INPUT =
export const REFERENCE_URLS_INPUT =
'[data-test-subj="detectionEngineStepAboutRuleReferenceUrls"] input';
export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]';
export const RISK_INPUT = '.euiRangeInput';
export const RISK_MAPPING_OVERRIDE_OPTION = '#risk_score-mapping-override';
@ -88,21 +96,29 @@ export const RULE_NAME_INPUT =
export const RULE_NAME_OVERRIDE = '[data-test-subj="detectionEngineStepAboutRuleRuleNameOverride"]';
export const RULE_STATUS = '[data-test-subj="ruleStatus"]';
export const RULE_TIMESTAMP_OVERRIDE =
'[data-test-subj="detectionEngineStepAboutRuleTimestampOverride"]';
export const RUNS_EVERY_INTERVAL =
'[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="interval"]';
export const RUNS_EVERY_TIME_TYPE =
'[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="timeType"]';
export const SCHEDULE_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]';
export const SCHEDULE_EDIT_TAB = '[data-test-subj="edit-rule-schedule-tab"]';
export const SCHEDULE_INTERVAL_AMOUNT_INPUT =
'[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="schedule-amount-input"]';
'[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="interval"]';
export const SCHEDULE_INTERVAL_UNITS_INPUT =
'[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="schedule-units-input"]';
'[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="timeType"]';
export const SCHEDULE_LOOKBACK_AMOUNT_INPUT =
'[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-amount-input"]';
'[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="timeType"]';
export const SCHEDULE_LOOKBACK_UNITS_INPUT =
'[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-units-input"]';

View file

@ -4,12 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const getDescriptionForTitle = (title: string) =>
cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION);
export const DETAILS_DESCRIPTION = '.euiDescriptionList__description';
export const DETAILS_TITLE = '.euiDescriptionList__title';
export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]';
export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]';
@ -17,9 +11,23 @@ export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggl
export const ABOUT_DETAILS =
'[data-test-subj="aboutRule"] [data-test-subj="listItemColumnStepRuleDescription"]';
export const ADDITIONAL_LOOK_BACK_DETAILS = 'Additional look-back time';
export const ANOMALY_SCORE_DETAILS = 'Anomaly score';
export const CUSTOM_QUERY_DETAILS = 'Custom query';
export const DEFINITION_DETAILS =
'[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"]';
export const DETAILS_DESCRIPTION = '.euiDescriptionList__description';
export const DETAILS_TITLE = '.euiDescriptionList__title';
export const FALSE_POSITIVES_DETAILS = 'False positive examples';
export const INDEX_PATTERNS_DETAILS = 'Index patterns';
export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown';
export const INVESTIGATION_NOTES_TOGGLE = 1;
@ -28,11 +36,38 @@ export const MACHINE_LEARNING_JOB_ID = '[data-test-subj="machineLearningJobId"]'
export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobStatus"]';
export const MITRE_ATTACK_DETAILS = 'MITRE ATT&CK';
export const RULE_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]';
export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]';
export const RULE_NAME_OVERRIDE_DETAILS = 'Rule name override';
export const RISK_SCORE_DETAILS = 'Risk score';
export const RISK_SCORE_OVERRIDE_DETAILS = 'Risk score override';
export const REFERENCE_URLS_DETAILS = 'Reference URLs';
export const RULE_TYPE_DETAILS = 'Rule type';
export const RUNS_EVERY_DETAILS = 'Runs every';
export const SCHEDULE_DETAILS =
'[data-test-subj=schedule] [data-test-subj="listItemColumnStepRuleDescription"]';
export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description';
export const SEVERITY_DETAILS = 'Severity';
export const TAGS_DETAILS = 'Tags';
export const THRESHOLD_DETAILS = 'Threshold';
export const TIMELINE_TEMPLATE_DETAILS = 'Timeline template';
export const TIMESTAMP_OVERRIDE_DETAILS = 'Timestamp override';
export const getDetails = (title: string) =>
cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION);

View file

@ -22,8 +22,10 @@ import {
OPEN_SELECTED_ALERTS_BTN,
MARK_ALERT_IN_PROGRESS_BTN,
MARK_SELECTED_ALERTS_IN_PROGRESS_BTN,
ALERT_RISK_SCORE_HEADER,
} from '../screens/alerts';
import { REFRESH_BUTTON } from '../screens/security_header';
import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline';
export const closeFirstAlert = () => {
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true });
@ -81,6 +83,12 @@ export const selectNumberOfAlerts = (numberOfAlerts: number) => {
}
};
export const sortRiskScore = () => {
cy.get(ALERT_RISK_SCORE_HEADER).click();
cy.get(TIMELINE_COLUMN_SPINNER).should('exist');
cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist');
};
export const investigateFirstAlertInTimeline = () => {
cy.get(SEND_ALERT_TO_TIMELINE_BTN).first().click({ force: true });
};

View file

@ -28,6 +28,8 @@ import {
IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK,
INPUT,
INVESTIGATION_NOTES_TEXTAREA,
LOOK_BACK_INTERVAL,
LOOK_BACK_TIME_TYPE,
MACHINE_LEARNING_DROPDOWN,
MACHINE_LEARNING_LIST,
MACHINE_LEARNING_TYPE,
@ -36,13 +38,17 @@ import {
MITRE_TACTIC_DROPDOWN,
MITRE_TECHNIQUES_INPUT,
REFERENCE_URLS_INPUT,
REFRESH_BUTTON,
RISK_INPUT,
RISK_MAPPING_OVERRIDE_OPTION,
RISK_OVERRIDE,
RULE_DESCRIPTION_INPUT,
RULE_NAME_INPUT,
RULE_NAME_OVERRIDE,
RULE_STATUS,
RULE_TIMESTAMP_OVERRIDE,
RUNS_EVERY_INTERVAL,
RUNS_EVERY_TIME_TYPE,
SCHEDULE_CONTINUE_BUTTON,
SCHEDULE_EDIT_TAB,
SEVERITY_DROPDOWN,
@ -190,6 +196,13 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = (
cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
};
export const fillScheduleRuleAndContinue = (rule: CustomRule | MachineLearningRule) => {
cy.get(RUNS_EVERY_INTERVAL).clear().type(rule.runsEvery.interval);
cy.get(RUNS_EVERY_TIME_TYPE).select(rule.runsEvery.timeType);
cy.get(LOOK_BACK_INTERVAL).clear().type(rule.lookBack.interval);
cy.get(LOOK_BACK_TIME_TYPE).select(rule.lookBack.timeType);
};
export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => {
const thresholdField = 0;
const threshold = 1;
@ -251,6 +264,14 @@ export const selectThresholdRuleType = () => {
cy.get(THRESHOLD_TYPE).click({ force: true });
};
export const waitForTheRuleToBeExecuted = async () => {
let status = '';
while (status !== 'succeeded') {
cy.get(REFRESH_BUTTON).click();
status = await cy.get(RULE_STATUS).invoke('text').promisify();
}
};
export const selectEqlRuleType = () => {
cy.get(EQL_TYPE).click({ force: true });
};

View file

@ -21,3 +21,7 @@ export const navigateFromHeaderTo = (page: string) => {
export const refreshPage = () => {
cy.get(REFRESH_BUTTON).click({ force: true }).invoke('text').should('not.equal', 'Updating');
};
export const waitForThePageToBeUpdated = () => {
cy.get(REFRESH_BUTTON).should('not.equal', 'Updating');
};

View file

@ -11,7 +11,7 @@
"cypress:open": "cypress open --config-file ./cypress/cypress.json",
"cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts",
"cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;",
"cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts",
"cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts",
"test:generate": "node scripts/endpoint/resolver_generator"
},
"devDependencies": {

View file

@ -69,7 +69,9 @@ const RuleStatusComponent: React.FC<RuleStatusProps> = ({ ruleId, ruleEnabled })
<>
<EuiFlexItem grow={false}>
<EuiHealth color={getStatusColor(currentStatus?.status ?? null)}>
<EuiText size="xs">{currentStatus?.status ?? getEmptyTagValue()}</EuiText>
<EuiText data-test-subj="ruleStatus" size="xs">
{currentStatus?.status ?? getEmptyTagValue()}
</EuiText>
</EuiHealth>
</EuiFlexItem>
{currentStatus?.status_date != null && currentStatus?.status != null && (
@ -84,6 +86,7 @@ const RuleStatusComponent: React.FC<RuleStatusProps> = ({ ruleId, ruleEnabled })
)}
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="refreshButton"
color="primary"
onClick={handleRefresh}
iconType="refresh"

View file

@ -145,21 +145,21 @@ export const ScheduleItem = ({
<EuiFormControlLayout
append={
<MyEuiSelect
data-test-subj="schedule-units-input"
fullWidth={false}
options={timeTypeOptions}
onChange={onChangeTimeType}
value={timeType}
data-test-subj="timeType"
{...rest}
/>
}
>
<EuiFieldNumber
data-test-subj="schedule-amount-input"
fullWidth
min={minimumValue}
onChange={onChangeTimeVal}
value={timeVal}
data-test-subj="interval"
{...rest}
/>
</EuiFormControlLayout>

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { EuiBreadcrumb, EuiBetaBadge } from '@elastic/eui';
import React, { memo } from 'react';
import React, { memo, useMemo } from 'react';
import { BetaHeader, ThemedBreadcrumbs } from './styles';
import { useColors } from '../use_colors';
@ -16,6 +16,15 @@ import { useColors } from '../use_colors';
* Breadcrumb menu
*/
export const Breadcrumbs = memo(function ({ breadcrumbs }: { breadcrumbs: EuiBreadcrumb[] }) {
// Just tagging the last crumb with `data-test-subj` for testing
const crumbsWithLastSubject: EuiBreadcrumb[] = useMemo(() => {
const lastcrumb = breadcrumbs.slice(-1).map((crumb) => {
crumb['data-test-subj'] = 'resolver:breadcrumbs:last';
return crumb;
});
return [...breadcrumbs.slice(0, -1), ...lastcrumb];
}, [breadcrumbs]);
const { resolverBreadcrumbBackground, resolverEdgeText } = useColors();
return (
<>
@ -32,7 +41,7 @@ export const Breadcrumbs = memo(function ({ breadcrumbs }: { breadcrumbs: EuiBre
<ThemedBreadcrumbs
background={resolverBreadcrumbBackground}
text={resolverEdgeText}
breadcrumbs={breadcrumbs}
breadcrumbs={crumbsWithLastSubject}
truncate={false}
/>
</>

View file

@ -42,6 +42,7 @@ import {
} from '../../../../../ingest_manager/common/types/models';
import { createV1SearchResponse, createV2SearchResponse } from './support/test_support';
import { PackageService } from '../../../../../ingest_manager/server/services';
import { metadataTransformPrefix } from '../../../../common/endpoint/constants';
describe('test endpoint route', () => {
let routerMock: jest.Mocked<IRouter>;
@ -175,7 +176,7 @@ describe('test endpoint route', () => {
type: ElasticsearchAssetType.indexTemplate,
},
{
id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0',
id: `${metadataTransformPrefix}-0.16.0-dev.0`,
type: ElasticsearchAssetType.transform,
},
])

View file

@ -8993,7 +8993,6 @@
"xpack.ingestManager.agentEnrollment.downloadLink": "elastic.co/downloadsに移動",
"xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "フリートに登録",
"xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "スタンドアロンモード",
"xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "エージェントを登録する前に、フリートを設定する必要があります。{link}",
"xpack.ingestManager.agentEnrollment.flyoutTitle": "エージェントの追加",
"xpack.ingestManager.agentEnrollment.managedDescription": "必要なエージェントの数に関係なく、Fleetでは、簡単に一元的に更新を管理し、エージェントにデプロイすることができます。次の手順に従い、Elasticエージェントをダウンロードし、Fleetに登録してください。",
"xpack.ingestManager.agentEnrollment.standaloneDescription": "スタンドアロンモードで実行中のエージェントは、構成を変更したい場合には、手動で更新する必要があります。次の手順に従い、スタンドアロンモードでElasticエージェントをダウンロードし、セットアップしてください。",
@ -9075,7 +9074,6 @@
"xpack.ingestManager.alphaMessging.closeFlyoutLabel": "閉じる",
"xpack.ingestManager.appNavigation.dataStreamsLinkText": "データセット",
"xpack.ingestManager.appNavigation.epmLinkText": "統合",
"xpack.ingestManager.appNavigation.fleetLinkText": "フリート",
"xpack.ingestManager.appNavigation.overviewLinkText": "概要",
"xpack.ingestManager.appNavigation.sendFeedbackButton": "フィードバックを送信",
"xpack.ingestManager.appNavigation.settingsButton": "設定",
@ -9085,9 +9083,6 @@
"xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "すべて",
"xpack.ingestManager.breadcrumbs.appTitle": "Ingest Manager",
"xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "データセット",
"xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle": "エージェント",
"xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle": "登録トークン",
"xpack.ingestManager.breadcrumbs.fleetPageTitle": "フリート",
"xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "インストール済み",
"xpack.ingestManager.breadcrumbs.integrationsPageTitle": "統合",
"xpack.ingestManager.breadcrumbs.overviewPageTitle": "概要",
@ -9156,8 +9151,6 @@
"xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "パッケージが見つかりません",
"xpack.ingestManager.epmList.searchPackagesPlaceholder": "統合を検索",
"xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "更新が可能です",
"xpack.ingestManager.fleet.pageSubtitle": "構成の更新を管理し、任意のサイズのエージェントのグループにデプロイします。",
"xpack.ingestManager.fleet.pageTitle": "フリート",
"xpack.ingestManager.genericActionsMenuText": "開く",
"xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去",
"xpack.ingestManager.homeIntegration.tutorialDirectory.ingestManagerAppButtonText": "Ingest Managerベータを試す",
@ -9236,7 +9229,6 @@
"xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "エージェントが収集するデータはさまざまなデータセットに整理されます。",
"xpack.ingestManager.overviewPageEnrollAgentButton": "エージェントの追加",
"xpack.ingestManager.overviewPageFleetPanelAction": "エージェントを表示",
"xpack.ingestManager.overviewPageFleetPanelTitle": "フリート",
"xpack.ingestManager.overviewPageFleetPanelTooltip": "Fleetを使用して、中央の場所からエージェントを登録し、構成を管理します。",
"xpack.ingestManager.overviewPageIntegrationsPanelAction": "統合を表示",
"xpack.ingestManager.overviewPageIntegrationsPanelTitle": "統合",

Some files were not shown because too many files have changed in this diff Show more