mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Merge branch '7.x' into backport/7.x/pr-78221
This commit is contained in:
commit
312cf73f11
114 changed files with 2970 additions and 3180 deletions
|
@ -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",
|
||||
|
|
2669
packages/kbn-pm/dist/index.js
vendored
2669
packages/kbn-pm/dist/index.js
vendored
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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}%)"
|
||||
|
|
|
@ -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' } },
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -639,7 +639,7 @@ Object {
|
|||
"body": Object {
|
||||
"aggs": Object {
|
||||
"stats": Object {
|
||||
"extended_stats": Object {
|
||||
"max": Object {
|
||||
"field": "transaction.duration.us",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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']
|
||||
>;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -102,6 +102,7 @@ describe('transaction queries', () => {
|
|||
traceId: 'qux',
|
||||
transactionId: 'quz',
|
||||
setup,
|
||||
searchAggregatedTransactions: false,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
}));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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()', () => {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
};
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import '../../__mocks__/kea.mock';
|
||||
import '../../__mocks__/react_router_history.mock';
|
||||
|
||||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -161,6 +161,9 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) {
|
|||
defaultMessage: 'Search Elastic',
|
||||
}),
|
||||
}}
|
||||
popoverProps={{
|
||||
repositionOnScroll: true,
|
||||
}}
|
||||
emptyMessage={
|
||||
<EuiSelectableMessage style={{ minHeight: 300 }}>
|
||||
<p>
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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')}>
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -47,7 +47,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => {
|
|||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.overviewPageTitle"
|
||||
defaultMessage="Ingest Manager"
|
||||
defaultMessage="Fleet"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}`;
|
||||
};
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 });
|
||||
|
|
@ -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 && (
|
||||
<>
|
||||
{' '}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
.documentsTab {
|
||||
&__documentField {
|
||||
position: relative;
|
||||
|
||||
&__button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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})`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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"]';
|
||||
|
|
|
@ -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"]';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
};
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue