mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -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",
|
"boom": "^7.2.0",
|
||||||
"chalk": "^2.4.2",
|
"chalk": "^2.4.2",
|
||||||
"check-disk-space": "^2.1.0",
|
"check-disk-space": "^2.1.0",
|
||||||
"chokidar": "3.2.1",
|
"chokidar": "^3.4.2",
|
||||||
"color": "1.0.3",
|
"color": "1.0.3",
|
||||||
"commander": "^3.0.2",
|
"commander": "^3.0.2",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.4",
|
||||||
|
@ -346,7 +346,7 @@
|
||||||
"angular-route": "^1.8.0",
|
"angular-route": "^1.8.0",
|
||||||
"angular-sortable-view": "^0.0.17",
|
"angular-sortable-view": "^0.0.17",
|
||||||
"archiver": "^3.1.1",
|
"archiver": "^3.1.1",
|
||||||
"axe-core": "^3.4.1",
|
"axe-core": "^4.0.2",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-jest": "^25.5.1",
|
"babel-jest": "^25.5.1",
|
||||||
"babel-plugin-istanbul": "^6.0.0",
|
"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",
|
"babel-loader": "^8.0.6",
|
||||||
"brace": "0.11.1",
|
"brace": "0.11.1",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"chokidar": "3.2.1",
|
"chokidar": "^3.4.2",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.4",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^3.4.2",
|
||||||
"expose-loader": "^0.7.5",
|
"expose-loader": "^0.7.5",
|
||||||
|
|
|
@ -227,9 +227,6 @@ export class ClusterManager {
|
||||||
fromRoot('src/legacy/server'),
|
fromRoot('src/legacy/server'),
|
||||||
fromRoot('src/legacy/ui'),
|
fromRoot('src/legacy/ui'),
|
||||||
fromRoot('src/legacy/utils'),
|
fromRoot('src/legacy/utils'),
|
||||||
fromRoot('x-pack/legacy/common'),
|
|
||||||
fromRoot('x-pack/legacy/plugins'),
|
|
||||||
fromRoot('x-pack/legacy/server'),
|
|
||||||
fromRoot('config'),
|
fromRoot('config'),
|
||||||
...extraPaths,
|
...extraPaths,
|
||||||
].map((path) => resolve(path))
|
].map((path) => resolve(path))
|
||||||
|
@ -242,7 +239,6 @@ export class ClusterManager {
|
||||||
/\.md$/,
|
/\.md$/,
|
||||||
/debug\.log$/,
|
/debug\.log$/,
|
||||||
...pluginInternalDirsIgnore,
|
...pluginInternalDirsIgnore,
|
||||||
fromRoot('src/legacy/server/sass/__tmp__'),
|
|
||||||
fromRoot('x-pack/plugins/reporting/chromium'),
|
fromRoot('x-pack/plugins/reporting/chromium'),
|
||||||
fromRoot('x-pack/plugins/security_solution/cypress'),
|
fromRoot('x-pack/plugins/security_solution/cypress'),
|
||||||
fromRoot('x-pack/plugins/apm/e2e'),
|
fromRoot('x-pack/plugins/apm/e2e'),
|
||||||
|
@ -253,7 +249,6 @@ export class ClusterManager {
|
||||||
fromRoot('x-pack/plugins/lists/server/scripts'),
|
fromRoot('x-pack/plugins/lists/server/scripts'),
|
||||||
fromRoot('x-pack/plugins/security_solution/scripts'),
|
fromRoot('x-pack/plugins/security_solution/scripts'),
|
||||||
fromRoot('x-pack/plugins/security_solution/server/lib/detection_engine/scripts'),
|
fromRoot('x-pack/plugins/security_solution/server/lib/detection_engine/scripts'),
|
||||||
'plugins/java_languageserver',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
this.watcher = chokidar.watch(watchPaths, {
|
this.watcher = chokidar.watch(watchPaths, {
|
||||||
|
|
|
@ -613,6 +613,7 @@ export class QueryStringInputUI extends Component<Props, State> {
|
||||||
})}
|
})}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded={this.state.isSuggestionsVisible}
|
aria-expanded={this.state.isSuggestionsVisible}
|
||||||
|
data-skip-axe="aria-required-children"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
role="search"
|
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(
|
const indexPattern = getStubIndexPattern(
|
||||||
'logstash-*',
|
'logstash-*',
|
||||||
(cfg: any) => cfg,
|
(cfg: any) => cfg,
|
||||||
|
@ -60,7 +70,9 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals
|
||||||
coreMock.createSetup()
|
coreMock.createSetup()
|
||||||
);
|
);
|
||||||
|
|
||||||
const field = new IndexPatternField(
|
const finalField =
|
||||||
|
field ??
|
||||||
|
new IndexPatternField(
|
||||||
{
|
{
|
||||||
name: 'bytes',
|
name: 'bytes',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
@ -76,7 +88,7 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
indexPattern,
|
indexPattern,
|
||||||
field,
|
field: finalField,
|
||||||
getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })),
|
getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })),
|
||||||
onAddFilter: jest.fn(),
|
onAddFilter: jest.fn(),
|
||||||
onAddField: jest.fn(),
|
onAddField: jest.fn(),
|
||||||
|
@ -91,18 +103,37 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals
|
||||||
|
|
||||||
describe('discover sidebar field', function () {
|
describe('discover sidebar field', function () {
|
||||||
it('should allow selecting fields', function () {
|
it('should allow selecting fields', function () {
|
||||||
const { comp, props } = getComponent();
|
const { comp, props } = getComponent({});
|
||||||
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
|
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
|
||||||
expect(props.onAddField).toHaveBeenCalledWith('bytes');
|
expect(props.onAddField).toHaveBeenCalledWith('bytes');
|
||||||
});
|
});
|
||||||
it('should allow deselecting fields', function () {
|
it('should allow deselecting fields', function () {
|
||||||
const { comp, props } = getComponent(true);
|
const { comp, props } = getComponent({ selected: true });
|
||||||
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
|
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
|
||||||
expect(props.onRemoveField).toHaveBeenCalledWith('bytes');
|
expect(props.onRemoveField).toHaveBeenCalledWith('bytes');
|
||||||
});
|
});
|
||||||
it('should trigger getDetails', function () {
|
it('should trigger getDetails', function () {
|
||||||
const { comp, props } = getComponent(true);
|
const { comp, props } = getComponent({ selected: true });
|
||||||
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
|
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
|
||||||
expect(props.getDetails).toHaveBeenCalledWith(props.field);
|
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 (
|
return (
|
||||||
<EuiPopover
|
<EuiPopover
|
||||||
ownFocus
|
ownFocus
|
||||||
|
@ -184,7 +197,7 @@ export function DiscoverField({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
togglePopover();
|
togglePopover();
|
||||||
}}
|
}}
|
||||||
buttonProps={{ 'data-test-subj': `field-${field.name}-showDetails` }}
|
dataTestSubj={`field-${field.name}-showDetails`}
|
||||||
fieldIcon={dscFieldIcon}
|
fieldIcon={dscFieldIcon}
|
||||||
fieldAction={actionButton}
|
fieldAction={actionButton}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
|
|
|
@ -19,8 +19,7 @@
|
||||||
|
|
||||||
import './field_button.scss';
|
import './field_button.scss';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { ReactNode, HTMLAttributes, ButtonHTMLAttributes } from 'react';
|
import React, { ReactNode, HTMLAttributes } from 'react';
|
||||||
import { CommonProps } from '@elastic/eui';
|
|
||||||
|
|
||||||
export interface FieldButtonProps extends HTMLAttributes<HTMLDivElement> {
|
export interface FieldButtonProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
/**
|
/**
|
||||||
|
@ -54,13 +53,10 @@ export interface FieldButtonProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
className?: string;
|
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;
|
onClick?: () => void;
|
||||||
/**
|
dataTestSubj?: string;
|
||||||
* Pass more button props to the actual `<button>` element
|
|
||||||
*/
|
|
||||||
buttonProps?: ButtonHTMLAttributes<HTMLButtonElement> & CommonProps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeToClassNameMap = {
|
const sizeToClassNameMap = {
|
||||||
|
@ -82,8 +78,7 @@ export function FieldButton({
|
||||||
className,
|
className,
|
||||||
isDraggable = false,
|
isDraggable = false,
|
||||||
onClick,
|
onClick,
|
||||||
buttonProps,
|
dataTestSubj,
|
||||||
...rest
|
|
||||||
}: FieldButtonProps) {
|
}: FieldButtonProps) {
|
||||||
const classes = classNames(
|
const classes = classNames(
|
||||||
'kbnFieldButton',
|
'kbnFieldButton',
|
||||||
|
@ -93,13 +88,9 @@ export function FieldButton({
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
const buttonClasses = classNames(
|
|
||||||
'kbn-resetFocusState kbnFieldButton__button',
|
|
||||||
buttonProps && buttonProps.className
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes} {...rest}>
|
<div className={classes}>
|
||||||
|
{onClick ? (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.type === 'click') {
|
if (e.type === 'click') {
|
||||||
|
@ -107,13 +98,21 @@ export function FieldButton({
|
||||||
}
|
}
|
||||||
onClick();
|
onClick();
|
||||||
}}
|
}}
|
||||||
{...buttonProps}
|
data-test-subj={dataTestSubj}
|
||||||
className={buttonClasses}
|
className={'kbn-resetFocusState kbnFieldButton__button'}
|
||||||
>
|
>
|
||||||
{fieldIcon && <span className="kbnFieldButton__fieldIcon">{fieldIcon}</span>}
|
{fieldIcon && <span className="kbnFieldButton__fieldIcon">{fieldIcon}</span>}
|
||||||
{fieldName && <span className="kbnFieldButton__name">{fieldName}</span>}
|
{fieldName && <span className="kbnFieldButton__name">{fieldName}</span>}
|
||||||
{fieldInfoIcon && <div className="kbnFieldButton__infoIcon">{fieldInfoIcon}</div>}
|
{fieldInfoIcon && <div className="kbnFieldButton__infoIcon">{fieldInfoIcon}</div>}
|
||||||
</button>
|
</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>}
|
{fieldAction && <div className="kbnFieldButton__fieldAction">{fieldAction}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -147,7 +147,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
<div
|
<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."
|
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"
|
class="euiModal euiModal--maxWidth-default visNewVisDialog"
|
||||||
role="menu"
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
@ -251,7 +250,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
class="euiKeyPadMenuItem visNewVisDialog__type"
|
class="euiKeyPadMenuItem visNewVisDialog__type"
|
||||||
data-test-subj="visType-visWithAliasUrl"
|
data-test-subj="visType-visWithAliasUrl"
|
||||||
data-vis-stage="alias"
|
data-vis-stage="alias"
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -283,7 +281,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
class="euiKeyPadMenuItem visNewVisDialog__type"
|
class="euiKeyPadMenuItem visNewVisDialog__type"
|
||||||
data-test-subj="visType-visWithSearch"
|
data-test-subj="visType-visWithSearch"
|
||||||
data-vis-stage="production"
|
data-vis-stage="production"
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -316,7 +313,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
data-test-subj="visType-vis"
|
data-test-subj="visType-vis"
|
||||||
data-vis-stage="production"
|
data-vis-stage="production"
|
||||||
disabled=""
|
disabled=""
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<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."
|
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"
|
className="visNewVisDialog"
|
||||||
onClose={[Function]}
|
onClose={[Function]}
|
||||||
role="menu"
|
|
||||||
>
|
>
|
||||||
<EuiFocusTrap>
|
<EuiFocusTrap>
|
||||||
<div
|
<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."
|
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"
|
className="euiModal euiModal--maxWidth-default visNewVisDialog"
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="menu"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<EuiI18n
|
<EuiI18n
|
||||||
|
@ -649,7 +643,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-describedby="visTypeDescription-visWithAliasUrl"
|
aria-describedby="visTypeDescription-visWithAliasUrl"
|
||||||
|
@ -662,7 +655,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -720,7 +712,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-describedby="visTypeDescription-visWithSearch"
|
aria-describedby="visTypeDescription-visWithSearch"
|
||||||
|
@ -733,7 +724,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -791,7 +781,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-describedby="visTypeDescription-vis"
|
aria-describedby="visTypeDescription-vis"
|
||||||
|
@ -804,7 +793,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -1060,7 +1048,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
<div
|
<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."
|
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"
|
class="euiModal euiModal--maxWidth-default visNewVisDialog"
|
||||||
role="menu"
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
@ -1148,7 +1135,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
class="euiKeyPadMenuItem visNewVisDialog__type"
|
class="euiKeyPadMenuItem visNewVisDialog__type"
|
||||||
data-test-subj="visType-vis"
|
data-test-subj="visType-vis"
|
||||||
data-vis-stage="production"
|
data-vis-stage="production"
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -1180,7 +1166,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
class="euiKeyPadMenuItem visNewVisDialog__type"
|
class="euiKeyPadMenuItem visNewVisDialog__type"
|
||||||
data-test-subj="visType-visWithAliasUrl"
|
data-test-subj="visType-visWithAliasUrl"
|
||||||
data-vis-stage="alias"
|
data-vis-stage="alias"
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -1212,7 +1197,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
class="euiKeyPadMenuItem visNewVisDialog__type"
|
class="euiKeyPadMenuItem visNewVisDialog__type"
|
||||||
data-test-subj="visType-visWithSearch"
|
data-test-subj="visType-visWithSearch"
|
||||||
data-vis-stage="production"
|
data-vis-stage="production"
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<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."
|
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"
|
className="visNewVisDialog"
|
||||||
onClose={[Function]}
|
onClose={[Function]}
|
||||||
role="menu"
|
|
||||||
>
|
>
|
||||||
<EuiFocusTrap>
|
<EuiFocusTrap>
|
||||||
<div
|
<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."
|
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"
|
className="euiModal euiModal--maxWidth-default visNewVisDialog"
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="menu"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<EuiI18n
|
<EuiI18n
|
||||||
|
@ -1494,7 +1476,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-describedby="visTypeDescription-vis"
|
aria-describedby="visTypeDescription-vis"
|
||||||
|
@ -1507,7 +1488,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -1565,7 +1545,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-describedby="visTypeDescription-visWithAliasUrl"
|
aria-describedby="visTypeDescription-visWithAliasUrl"
|
||||||
|
@ -1578,7 +1557,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -1636,7 +1614,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-describedby="visTypeDescription-visWithSearch"
|
aria-describedby="visTypeDescription-visWithSearch"
|
||||||
|
@ -1649,7 +1626,6 @@ exports[`NewVisModal should render as expected 1`] = `
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="menuitem"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -108,7 +108,6 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
|
||||||
onClose={this.onCloseModal}
|
onClose={this.onCloseModal}
|
||||||
className="visNewVisDialog"
|
className="visNewVisDialog"
|
||||||
aria-label={visNewVisDialogAriaLabel}
|
aria-label={visNewVisDialogAriaLabel}
|
||||||
role="menu"
|
|
||||||
>
|
>
|
||||||
<TypeSelection
|
<TypeSelection
|
||||||
showExperimental={this.isLabsEnabled}
|
showExperimental={this.isLabsEnabled}
|
||||||
|
|
|
@ -259,7 +259,6 @@ class TypeSelection extends React.Component<TypeSelectionProps, TypeSelectionSta
|
||||||
data-vis-stage={!('aliasPath' in visType) ? visType.stage : 'alias'}
|
data-vis-stage={!('aliasPath' in visType) ? visType.stage : 'alias'}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
aria-describedby={`visTypeDescription-${visType.name}`}
|
aria-describedby={`visTypeDescription-${visType.name}`}
|
||||||
role="menuitem"
|
|
||||||
{...stage}
|
{...stage}
|
||||||
>
|
>
|
||||||
<VisTypeIcon
|
<VisTypeIcon
|
||||||
|
|
|
@ -36,6 +36,8 @@ interface TestOptions {
|
||||||
|
|
||||||
export const normalizeResult = (report: any) => {
|
export const normalizeResult = (report: any) => {
|
||||||
if (report.error) {
|
if (report.error) {
|
||||||
|
const error = new Error(report.error.message);
|
||||||
|
error.stack = report.error.stack;
|
||||||
throw report.error;
|
throw report.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +73,6 @@ export function A11yProvider({ getService }: FtrProviderContext) {
|
||||||
.concat(excludeTestSubj || [])
|
.concat(excludeTestSubj || [])
|
||||||
.map((ts) => [testSubjectToCss(ts)])
|
.map((ts) => [testSubjectToCss(ts)])
|
||||||
.concat([
|
.concat([
|
||||||
['.ace_scrollbar'],
|
|
||||||
[
|
[
|
||||||
'.leaflet-vega-container[role="graphics-document"][aria-roledescription="visualization"]',
|
'.leaflet-vega-container[role="graphics-document"][aria-roledescription="visualization"]',
|
||||||
],
|
],
|
||||||
|
@ -97,7 +98,7 @@ export function A11yProvider({ getService }: FtrProviderContext) {
|
||||||
runOnly: ['wcag2a', 'wcag2aa'],
|
runOnly: ['wcag2a', 'wcag2aa'],
|
||||||
rules: {
|
rules: {
|
||||||
'color-contrast': {
|
'color-contrast': {
|
||||||
enabled: false,
|
enabled: false, // disabled because we have too many failures
|
||||||
},
|
},
|
||||||
bypass: {
|
bypass: {
|
||||||
enabled: false, // disabled because it's too flaky
|
enabled: false, // disabled because it's too flaky
|
||||||
|
|
|
@ -23,6 +23,18 @@ export function analyzeWithAxe(context, options, callback) {
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (window.axe) {
|
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);
|
return window.axe.run(context, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +43,14 @@ export function analyzeWithAxe(context, options, callback) {
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(result) => callback({ result }),
|
(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}>
|
<StyledSpan darkMode={darkMode}>
|
||||||
<PaletteLegend color={color}>
|
<PaletteLegend color={color}>
|
||||||
<EuiText size="xs">
|
<EuiText size="xs">
|
||||||
{labels[ind]} ({ranks?.[ind]}%)
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.apm.rum.coreVitals.paletteLegend.rankPercentage"
|
id="xpack.apm.rum.coreVitals.paletteLegend.rankPercentage"
|
||||||
defaultMessage="{labelsInd} ({ranksInd}%)"
|
defaultMessage="{labelsInd} ({ranksInd}%)"
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getFormattedBuckets } from '../index';
|
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', () => {
|
describe('Distribution', () => {
|
||||||
it('getFormattedBuckets', () => {
|
it('getFormattedBuckets', () => {
|
||||||
|
@ -20,6 +18,7 @@ describe('Distribution', () => {
|
||||||
samples: [
|
samples: [
|
||||||
{
|
{
|
||||||
transactionId: 'someTransactionId',
|
transactionId: 'someTransactionId',
|
||||||
|
traceId: 'someTraceId',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -29,10 +28,12 @@ describe('Distribution', () => {
|
||||||
samples: [
|
samples: [
|
||||||
{
|
{
|
||||||
transactionId: 'anotherTransactionId',
|
transactionId: 'anotherTransactionId',
|
||||||
|
traceId: 'anotherTraceId',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as IBucket[];
|
];
|
||||||
|
|
||||||
expect(getFormattedBuckets(buckets, 20)).toEqual([
|
expect(getFormattedBuckets(buckets, 20)).toEqual([
|
||||||
{ x: 20, x0: 0, y: 0, style: { cursor: 'default' } },
|
{ x: 20, x0: 0, y: 0, style: { cursor: 'default' } },
|
||||||
{ x: 40, x0: 20, 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
|
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||||
import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
|
import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
// 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 { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||||
import { getDurationFormatter } from '../../../../utils/formatters';
|
import { getDurationFormatter } from '../../../../utils/formatters';
|
||||||
// @ts-expect-error
|
// @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) {
|
if (!buckets) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { Location } from 'history';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
// 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 { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||||
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
|
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
|
||||||
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
||||||
|
@ -34,7 +34,7 @@ interface Props {
|
||||||
waterfall: IWaterfall;
|
waterfall: IWaterfall;
|
||||||
exceedsMax: boolean;
|
exceedsMax: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
traceSamples: IBucket['samples'];
|
traceSamples: DistributionBucket['samples'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WaterfallWithSummmary({
|
export function WaterfallWithSummmary({
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 { useHistory, useParams } from 'react-router-dom';
|
||||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||||
import { useFetcher } from './useFetcher';
|
import { useFetcher } from './useFetcher';
|
||||||
|
@ -69,11 +69,11 @@ export function useTransactionDistribution(urlParams: IUrlParams) {
|
||||||
// selected sample was not found. select a new one:
|
// selected sample was not found. select a new one:
|
||||||
// sorted by total number of requests, but only pick
|
// sorted by total number of requests, but only pick
|
||||||
// from buckets that have samples
|
// from buckets that have samples
|
||||||
const preferredSample = maybe(
|
const bucketsSortedByCount = response.buckets
|
||||||
response.buckets
|
.filter((bucket) => !isEmpty(bucket.samples))
|
||||||
.filter((bucket) => bucket.samples.length > 0)
|
.sort((bucket) => bucket.count);
|
||||||
.sort((bucket) => bucket.count)[0]?.samples[0]
|
|
||||||
);
|
const preferredSample = maybe(bucketsSortedByCount[0]?.samples[0]);
|
||||||
|
|
||||||
history.push({
|
history.push({
|
||||||
...history.location,
|
...history.location,
|
||||||
|
|
|
@ -639,7 +639,7 @@ Object {
|
||||||
"body": Object {
|
"body": Object {
|
||||||
"aggs": Object {
|
"aggs": Object {
|
||||||
"stats": Object {
|
"stats": Object {
|
||||||
"extended_stats": Object {
|
"max": Object {
|
||||||
"field": "transaction.duration.us",
|
"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,26 +3,52 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with 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 {
|
import {
|
||||||
Setup,
|
Setup,
|
||||||
SetupTimeRange,
|
SetupTimeRange,
|
||||||
SetupUIFilters,
|
SetupUIFilters,
|
||||||
} from '../../../helpers/setup_request';
|
} from '../../../helpers/setup_request';
|
||||||
import { bucketFetcher } from './fetcher';
|
import {
|
||||||
import { bucketTransformer } from './transform';
|
getDocumentTypeFilterForAggregatedTransactions,
|
||||||
|
getProcessorEventForAggregatedTransactions,
|
||||||
|
getTransactionDurationFieldForAggregatedTransactions,
|
||||||
|
} from '../../../helpers/aggregated_transactions';
|
||||||
|
|
||||||
export async function getBuckets(
|
function getHistogramAggOptions({
|
||||||
serviceName: string,
|
bucketSize,
|
||||||
transactionName: string,
|
field,
|
||||||
transactionType: string,
|
distributionMax,
|
||||||
transactionId: string,
|
}: {
|
||||||
traceId: string,
|
bucketSize: number;
|
||||||
distributionMax: number,
|
field: string;
|
||||||
bucketSize: number,
|
distributionMax: number;
|
||||||
setup: Setup & SetupTimeRange & SetupUIFilters
|
}) {
|
||||||
) {
|
return {
|
||||||
const response = await bucketFetcher(
|
field,
|
||||||
|
interval: bucketSize,
|
||||||
|
min_doc_count: 0,
|
||||||
|
extended_bounds: {
|
||||||
|
min: 0,
|
||||||
|
max: distributionMax,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBuckets({
|
||||||
serviceName,
|
serviceName,
|
||||||
transactionName,
|
transactionName,
|
||||||
transactionType,
|
transactionType,
|
||||||
|
@ -30,8 +56,151 @@ export async function getBuckets(
|
||||||
traceId,
|
traceId,
|
||||||
distributionMax,
|
distributionMax,
|
||||||
bucketSize,
|
bucketSize,
|
||||||
setup
|
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;
|
||||||
|
|
||||||
return bucketTransformer(response);
|
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.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
|
||||||
import {
|
import {
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
TRANSACTION_DURATION,
|
|
||||||
TRANSACTION_NAME,
|
TRANSACTION_NAME,
|
||||||
TRANSACTION_TYPE,
|
TRANSACTION_TYPE,
|
||||||
} from '../../../../common/elasticsearch_fieldnames';
|
} from '../../../../common/elasticsearch_fieldnames';
|
||||||
|
@ -16,18 +14,33 @@ import {
|
||||||
SetupTimeRange,
|
SetupTimeRange,
|
||||||
SetupUIFilters,
|
SetupUIFilters,
|
||||||
} from '../../helpers/setup_request';
|
} from '../../helpers/setup_request';
|
||||||
|
import {
|
||||||
|
getProcessorEventForAggregatedTransactions,
|
||||||
|
getTransactionDurationFieldForAggregatedTransactions,
|
||||||
|
} from '../../helpers/aggregated_transactions';
|
||||||
|
|
||||||
export async function getDistributionMax(
|
export async function getDistributionMax({
|
||||||
serviceName: string,
|
serviceName,
|
||||||
transactionName: string,
|
transactionName,
|
||||||
transactionType: string,
|
transactionType,
|
||||||
setup: Setup & SetupTimeRange & SetupUIFilters
|
setup,
|
||||||
) {
|
searchAggregatedTransactions,
|
||||||
|
}: {
|
||||||
|
serviceName: string;
|
||||||
|
transactionName: string;
|
||||||
|
transactionType: string;
|
||||||
|
setup: Setup & SetupTimeRange & SetupUIFilters;
|
||||||
|
searchAggregatedTransactions: boolean;
|
||||||
|
}) {
|
||||||
const { start, end, uiFiltersES, apmEventClient } = setup;
|
const { start, end, uiFiltersES, apmEventClient } = setup;
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
apm: {
|
apm: {
|
||||||
events: [ProcessorEvent.transaction],
|
events: [
|
||||||
|
getProcessorEventForAggregatedTransactions(
|
||||||
|
searchAggregatedTransactions
|
||||||
|
),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
size: 0,
|
size: 0,
|
||||||
|
@ -52,8 +65,10 @@ export async function getDistributionMax(
|
||||||
},
|
},
|
||||||
aggs: {
|
aggs: {
|
||||||
stats: {
|
stats: {
|
||||||
extended_stats: {
|
max: {
|
||||||
field: TRANSACTION_DURATION,
|
field: getTransactionDurationFieldForAggregatedTransactions(
|
||||||
|
searchAggregatedTransactions
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -61,5 +76,5 @@ export async function getDistributionMax(
|
||||||
};
|
};
|
||||||
|
|
||||||
const resp = await apmEventClient.search(params);
|
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,
|
transactionId,
|
||||||
traceId,
|
traceId,
|
||||||
setup,
|
setup,
|
||||||
|
searchAggregatedTransactions,
|
||||||
}: {
|
}: {
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
transactionName: string;
|
transactionName: string;
|
||||||
|
@ -39,20 +40,23 @@ export async function getTransactionDistribution({
|
||||||
transactionId: string;
|
transactionId: string;
|
||||||
traceId: string;
|
traceId: string;
|
||||||
setup: Setup & SetupTimeRange & SetupUIFilters;
|
setup: Setup & SetupTimeRange & SetupUIFilters;
|
||||||
|
searchAggregatedTransactions: boolean;
|
||||||
}) {
|
}) {
|
||||||
const distributionMax = await getDistributionMax(
|
const distributionMax = await getDistributionMax({
|
||||||
serviceName,
|
serviceName,
|
||||||
transactionName,
|
transactionName,
|
||||||
transactionType,
|
transactionType,
|
||||||
setup
|
setup,
|
||||||
);
|
searchAggregatedTransactions,
|
||||||
|
});
|
||||||
|
|
||||||
if (distributionMax == null) {
|
if (distributionMax == null) {
|
||||||
return { noHits: true, buckets: [], bucketSize: 0 };
|
return { noHits: true, buckets: [], bucketSize: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bucketSize = getBucketSize(distributionMax);
|
const bucketSize = getBucketSize(distributionMax);
|
||||||
const { buckets, noHits } = await getBuckets(
|
|
||||||
|
const { buckets, noHits } = await getBuckets({
|
||||||
serviceName,
|
serviceName,
|
||||||
transactionName,
|
transactionName,
|
||||||
transactionType,
|
transactionType,
|
||||||
|
@ -60,8 +64,9 @@ export async function getTransactionDistribution({
|
||||||
traceId,
|
traceId,
|
||||||
distributionMax,
|
distributionMax,
|
||||||
bucketSize,
|
bucketSize,
|
||||||
setup
|
setup,
|
||||||
);
|
searchAggregatedTransactions,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
noHits,
|
noHits,
|
||||||
|
|
|
@ -102,6 +102,7 @@ describe('transaction queries', () => {
|
||||||
traceId: 'qux',
|
traceId: 'qux',
|
||||||
transactionId: 'quz',
|
transactionId: 'quz',
|
||||||
setup,
|
setup,
|
||||||
|
searchAggregatedTransactions: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,10 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({
|
||||||
traceId = '',
|
traceId = '',
|
||||||
} = context.params.query;
|
} = context.params.query;
|
||||||
|
|
||||||
|
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
|
||||||
|
setup
|
||||||
|
);
|
||||||
|
|
||||||
return getTransactionDistribution({
|
return getTransactionDistribution({
|
||||||
serviceName,
|
serviceName,
|
||||||
transactionType,
|
transactionType,
|
||||||
|
@ -131,6 +135,7 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({
|
||||||
transactionId,
|
transactionId,
|
||||||
traceId,
|
traceId,
|
||||||
setup,
|
setup,
|
||||||
|
searchAggregatedTransactions,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -4,8 +4,11 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { mockHistory } from './';
|
||||||
|
|
||||||
export const mockKibanaValues = {
|
export const mockKibanaValues = {
|
||||||
config: { host: 'http://localhost:3002' },
|
config: { host: 'http://localhost:3002' },
|
||||||
|
history: mockHistory,
|
||||||
navigateToUrl: jest.fn(),
|
navigateToUrl: jest.fn(),
|
||||||
setBreadcrumbs: jest.fn(),
|
setBreadcrumbs: jest.fn(),
|
||||||
setDocTitle: jest.fn(),
|
setDocTitle: jest.fn(),
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const mockHistory = {
|
||||||
pathname: '/current-path',
|
pathname: '/current-path',
|
||||||
},
|
},
|
||||||
listen: jest.fn(() => jest.fn()),
|
listen: jest.fn(() => jest.fn()),
|
||||||
};
|
} as any;
|
||||||
export const mockLocation = {
|
export const mockLocation = {
|
||||||
key: 'someKey',
|
key: 'someKey',
|
||||||
pathname: '/current-path',
|
pathname: '/current-path',
|
||||||
|
@ -25,6 +25,7 @@ export const mockLocation = {
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...(jest.requireActual('react-router-dom') as object),
|
||||||
useHistory: jest.fn(() => mockHistory),
|
useHistory: jest.fn(() => mockHistory),
|
||||||
useLocation: jest.fn(() => mockLocation),
|
useLocation: jest.fn(() => mockLocation),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -41,6 +41,7 @@ export const renderApp = (
|
||||||
|
|
||||||
const unmountKibanaLogic = mountKibanaLogic({
|
const unmountKibanaLogic = mountKibanaLogic({
|
||||||
config,
|
config,
|
||||||
|
history: params.history,
|
||||||
navigateToUrl: core.application.navigateToUrl,
|
navigateToUrl: core.application.navigateToUrl,
|
||||||
setBreadcrumbs: core.chrome.setBreadcrumbs,
|
setBreadcrumbs: core.chrome.setBreadcrumbs,
|
||||||
setDocTitle: core.chrome.docTitle.change,
|
setDocTitle: core.chrome.docTitle.change,
|
||||||
|
@ -53,9 +54,7 @@ export const renderApp = (
|
||||||
errorConnecting,
|
errorConnecting,
|
||||||
readOnlyMode: initialData.readOnlyMode,
|
readOnlyMode: initialData.readOnlyMode,
|
||||||
});
|
});
|
||||||
const unmountFlashMessagesLogic = mountFlashMessagesLogic({
|
const unmountFlashMessagesLogic = mountFlashMessagesLogic();
|
||||||
history: params.history,
|
|
||||||
});
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
|
|
|
@ -7,11 +7,14 @@
|
||||||
import { resetContext } from 'kea';
|
import { resetContext } from 'kea';
|
||||||
|
|
||||||
import { mockHistory } from '../../__mocks__';
|
import { mockHistory } from '../../__mocks__';
|
||||||
|
jest.mock('../kibana', () => ({
|
||||||
|
KibanaLogic: { values: { history: mockHistory } },
|
||||||
|
}));
|
||||||
|
|
||||||
import { FlashMessagesLogic, mountFlashMessagesLogic, IFlashMessage } from './';
|
import { FlashMessagesLogic, mountFlashMessagesLogic, IFlashMessage } from './';
|
||||||
|
|
||||||
describe('FlashMessagesLogic', () => {
|
describe('FlashMessagesLogic', () => {
|
||||||
const mount = () => mountFlashMessagesLogic({ history: mockHistory as any });
|
const mount = () => mountFlashMessagesLogic();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
|
|
||||||
import { kea, MakeLogicType } from 'kea';
|
import { kea, MakeLogicType } from 'kea';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { History } from 'history';
|
|
||||||
|
import { KibanaLogic } from '../kibana';
|
||||||
|
|
||||||
export interface IFlashMessage {
|
export interface IFlashMessage {
|
||||||
type: 'success' | 'info' | 'warning' | 'error';
|
type: 'success' | 'info' | 'warning' | 'error';
|
||||||
|
@ -61,10 +62,10 @@ export const FlashMessagesLogic = kea<MakeLogicType<IFlashMessagesValues, IFlash
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
events: ({ props, values, actions }) => ({
|
events: ({ values, actions }) => ({
|
||||||
afterMount: () => {
|
afterMount: () => {
|
||||||
// On React Router navigation, clear previous flash messages and load any queued messages
|
// 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.clearFlashMessages();
|
||||||
actions.setFlashMessages(values.queuedMessages);
|
actions.setFlashMessages(values.queuedMessages);
|
||||||
actions.clearQueuedMessages();
|
actions.clearQueuedMessages();
|
||||||
|
@ -81,11 +82,7 @@ export const FlashMessagesLogic = kea<MakeLogicType<IFlashMessagesValues, IFlash
|
||||||
/**
|
/**
|
||||||
* Mount/props helper
|
* Mount/props helper
|
||||||
*/
|
*/
|
||||||
interface IFlashMessagesLogicProps {
|
export const mountFlashMessagesLogic = () => {
|
||||||
history: History;
|
|
||||||
}
|
|
||||||
export const mountFlashMessagesLogic = (props: IFlashMessagesLogicProps) => {
|
|
||||||
FlashMessagesLogic(props);
|
|
||||||
const unmount = FlashMessagesLogic.mount();
|
const unmount = FlashMessagesLogic.mount();
|
||||||
return unmount;
|
return unmount;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mockHistory } from '../../__mocks__';
|
import { mockHistory } from '../../__mocks__';
|
||||||
|
jest.mock('../kibana', () => ({
|
||||||
|
KibanaLogic: { values: { history: mockHistory } },
|
||||||
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FlashMessagesLogic,
|
FlashMessagesLogic,
|
||||||
|
@ -18,7 +21,7 @@ describe('Flash Message Helpers', () => {
|
||||||
const message = 'I am a message';
|
const message = 'I am a message';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mountFlashMessagesLogic({ history: mockHistory as any });
|
mountFlashMessagesLogic();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('setSuccessMessage()', () => {
|
it('setSuccessMessage()', () => {
|
||||||
|
|
|
@ -20,7 +20,10 @@ describe('KibanaLogic', () => {
|
||||||
it('sets values from props', () => {
|
it('sets values from props', () => {
|
||||||
mountKibanaLogic(mockKibanaValues);
|
mountKibanaLogic(mockKibanaValues);
|
||||||
|
|
||||||
expect(KibanaLogic.values).toEqual(mockKibanaValues);
|
expect(KibanaLogic.values).toEqual({
|
||||||
|
...mockKibanaValues,
|
||||||
|
navigateToUrl: expect.any(Function),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gracefully handles missing configs', () => {
|
it('gracefully handles missing configs', () => {
|
||||||
|
@ -29,4 +32,20 @@ describe('KibanaLogic', () => {
|
||||||
expect(KibanaLogic.values.config).toEqual({});
|
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 { kea, MakeLogicType } from 'kea';
|
||||||
|
|
||||||
|
import { History } from 'history';
|
||||||
import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public';
|
import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public';
|
||||||
|
|
||||||
export interface IKibanaValues {
|
import { createHref, ICreateHrefOptions } from '../react_router_helpers';
|
||||||
|
|
||||||
|
interface IKibanaLogicProps {
|
||||||
config: { host?: string };
|
config: { host?: string };
|
||||||
|
history: History;
|
||||||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||||
setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void;
|
setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void;
|
||||||
setDocTitle(title: string): void;
|
setDocTitle(title: string): void;
|
||||||
}
|
}
|
||||||
|
export interface IKibanaValues extends IKibanaLogicProps {
|
||||||
|
navigateToUrl(path: string, options?: ICreateHrefOptions): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export const KibanaLogic = kea<MakeLogicType<IKibanaValues>>({
|
export const KibanaLogic = kea<MakeLogicType<IKibanaValues>>({
|
||||||
path: ['enterprise_search', 'kibana_logic'],
|
path: ['enterprise_search', 'kibana_logic'],
|
||||||
reducers: ({ props }) => ({
|
reducers: ({ props }) => ({
|
||||||
config: [props.config || {}, {}],
|
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, {}],
|
setBreadcrumbs: [props.setBreadcrumbs, {}],
|
||||||
setDocTitle: [props.setDocTitle, {}],
|
setDocTitle: [props.setDocTitle, {}],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mountKibanaLogic = (props: IKibanaValues) => {
|
export const mountKibanaLogic = (props: IKibanaLogicProps) => {
|
||||||
KibanaLogic(props);
|
KibanaLogic(props);
|
||||||
const unmount = KibanaLogic.mount();
|
const unmount = KibanaLogic.mount();
|
||||||
return unmount;
|
return unmount;
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '../../__mocks__/kea.mock';
|
import '../../__mocks__/kea.mock';
|
||||||
import '../../__mocks__/react_router_history.mock';
|
|
||||||
import { mockKibanaValues, mockHistory } from '../../__mocks__';
|
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 { letBrowserHandleEvent } from '../react_router_helpers';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -50,21 +52,23 @@ describe('useBreadcrumbs', () => {
|
||||||
|
|
||||||
it('prevents default navigation and uses React Router history on click', () => {
|
it('prevents default navigation and uses React Router history on click', () => {
|
||||||
const breadcrumb = useBreadcrumbs([{ text: '', path: '/test' }])[0] as any;
|
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() };
|
const event = { preventDefault: jest.fn() };
|
||||||
breadcrumb.onClick(event);
|
breadcrumb.onClick(event);
|
||||||
|
|
||||||
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test');
|
|
||||||
expect(mockHistory.createHref).toHaveBeenCalled();
|
|
||||||
expect(event.preventDefault).toHaveBeenCalled();
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not call createHref if shouldNotCreateHref is passed', () => {
|
it('does not call createHref if shouldNotCreateHref is passed', () => {
|
||||||
const breadcrumb = useBreadcrumbs([
|
const breadcrumb = useBreadcrumbs([
|
||||||
{ text: '', path: '/test', shouldNotCreateHref: true },
|
{ text: '', path: '/test', shouldNotCreateHref: true },
|
||||||
])[0] as any;
|
])[0] as any;
|
||||||
breadcrumb.onClick({ preventDefault: () => null });
|
|
||||||
|
|
||||||
expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test');
|
expect(breadcrumb.href).toEqual('/test');
|
||||||
expect(mockHistory.createHref).not.toHaveBeenCalled();
|
expect(mockHistory.createHref).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useValues } from 'kea';
|
import { useValues } from 'kea';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { EuiBreadcrumb } from '@elastic/eui';
|
import { EuiBreadcrumb } from '@elastic/eui';
|
||||||
|
|
||||||
import { KibanaLogic } from '../../shared/kibana';
|
import { KibanaLogic } from '../../shared/kibana';
|
||||||
|
@ -16,7 +15,7 @@ import {
|
||||||
WORKPLACE_SEARCH_PLUGIN,
|
WORKPLACE_SEARCH_PLUGIN,
|
||||||
} from '../../../../common/constants';
|
} from '../../../../common/constants';
|
||||||
|
|
||||||
import { letBrowserHandleEvent } from '../react_router_helpers';
|
import { letBrowserHandleEvent, createHref } from '../react_router_helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate React-Router-friendly EUI breadcrumb objects
|
* Generate React-Router-friendly EUI breadcrumb objects
|
||||||
|
@ -33,20 +32,17 @@ interface IBreadcrumb {
|
||||||
export type TBreadcrumbs = IBreadcrumb[];
|
export type TBreadcrumbs = IBreadcrumb[];
|
||||||
|
|
||||||
export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => {
|
export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => {
|
||||||
const history = useHistory();
|
const { navigateToUrl, history } = useValues(KibanaLogic);
|
||||||
const { navigateToUrl } = useValues(KibanaLogic);
|
|
||||||
|
|
||||||
return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => {
|
return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => {
|
||||||
const breadcrumb = { text } as EuiBreadcrumb;
|
const breadcrumb = { text } as EuiBreadcrumb;
|
||||||
|
|
||||||
if (path) {
|
if (path) {
|
||||||
const href = shouldNotCreateHref ? path : (history.createHref({ pathname: path }) as string);
|
breadcrumb.href = createHref(path, history, { shouldNotCreateHref });
|
||||||
|
|
||||||
breadcrumb.href = href;
|
|
||||||
breadcrumb.onClick = (event) => {
|
breadcrumb.onClick = (event) => {
|
||||||
if (letBrowserHandleEvent(event)) return;
|
if (letBrowserHandleEvent(event)) return;
|
||||||
event.preventDefault();
|
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__/kea.mock';
|
||||||
import '../../__mocks__/react_router_history.mock';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow, mount } from 'enzyme';
|
import { shallow, mount } from 'enzyme';
|
||||||
|
|
|
@ -6,11 +6,10 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useValues } from 'kea';
|
import { useValues } from 'kea';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui';
|
import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui';
|
||||||
|
|
||||||
import { KibanaLogic } from '../../shared/kibana';
|
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
|
* Generates either an EuiLink or EuiButton with a React-Router-ified link
|
||||||
|
@ -33,11 +32,10 @@ export const EuiReactRouterHelper: React.FC<IEuiReactRouterProps> = ({
|
||||||
shouldNotCreateHref,
|
shouldNotCreateHref,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const history = useHistory();
|
const { navigateToUrl, history } = useValues(KibanaLogic);
|
||||||
const { navigateToUrl } = useValues(KibanaLogic);
|
|
||||||
|
|
||||||
// Generate the correct link href (with basename etc. accounted for)
|
// 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) => {
|
const reactRouterLinkClick = (event: React.MouseEvent) => {
|
||||||
if (onClick) onClick(); // Run any passed click events (e.g. telemetry)
|
if (onClick) onClick(); // Run any passed click events (e.g. telemetry)
|
||||||
|
@ -47,7 +45,7 @@ export const EuiReactRouterHelper: React.FC<IEuiReactRouterProps> = ({
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Perform SPA navigation.
|
// Perform SPA navigation.
|
||||||
navigateToUrl(href);
|
navigateToUrl(to, { shouldNotCreateHref });
|
||||||
};
|
};
|
||||||
|
|
||||||
const reactRouterProps = { href, onClick: reactRouterLinkClick };
|
const reactRouterProps = { href, onClick: reactRouterLinkClick };
|
||||||
|
|
|
@ -5,5 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { letBrowserHandleEvent } from './link_events';
|
export { letBrowserHandleEvent } from './link_events';
|
||||||
|
export { createHref, ICreateHrefOptions } from './create_href';
|
||||||
export { EuiReactRouterLink as EuiLink } from './eui_link';
|
export { EuiReactRouterLink as EuiLink } from './eui_link';
|
||||||
export { EuiReactRouterButton as EuiButton } from './eui_link';
|
export { EuiReactRouterButton as EuiButton } from './eui_link';
|
||||||
|
|
|
@ -161,6 +161,9 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) {
|
||||||
defaultMessage: 'Search Elastic',
|
defaultMessage: 'Search Elastic',
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
popoverProps={{
|
||||||
|
repositionOnScroll: true,
|
||||||
|
}}
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
<EuiSelectableMessage style={{ minHeight: 300 }}>
|
<EuiSelectableMessage style={{ minHeight: 300 }}>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useCore } from './use_core';
|
||||||
const BASE_BREADCRUMB: ChromeBreadcrumb = {
|
const BASE_BREADCRUMB: ChromeBreadcrumb = {
|
||||||
href: pagePathGetters.overview(),
|
href: pagePathGetters.overview(),
|
||||||
text: i18n.translate('xpack.ingestManager.breadcrumbs.appTitle', {
|
text: i18n.translate('xpack.ingestManager.breadcrumbs.appTitle', {
|
||||||
defaultMessage: 'Ingest Manager',
|
defaultMessage: 'Fleet',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -155,21 +155,15 @@ const breadcrumbGetters: {
|
||||||
fleet: () => [
|
fleet: () => [
|
||||||
BASE_BREADCRUMB,
|
BASE_BREADCRUMB,
|
||||||
{
|
{
|
||||||
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', {
|
text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', {
|
||||||
defaultMessage: 'Fleet',
|
defaultMessage: 'Agents',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
fleet_agent_list: () => [
|
fleet_agent_list: () => [
|
||||||
BASE_BREADCRUMB,
|
BASE_BREADCRUMB,
|
||||||
{
|
{
|
||||||
href: pagePathGetters.fleet(),
|
text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', {
|
||||||
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', {
|
|
||||||
defaultMessage: 'Fleet',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', {
|
|
||||||
defaultMessage: 'Agents',
|
defaultMessage: 'Agents',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -178,12 +172,7 @@ const breadcrumbGetters: {
|
||||||
BASE_BREADCRUMB,
|
BASE_BREADCRUMB,
|
||||||
{
|
{
|
||||||
href: pagePathGetters.fleet(),
|
href: pagePathGetters.fleet(),
|
||||||
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', {
|
text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', {
|
||||||
defaultMessage: 'Fleet',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', {
|
|
||||||
defaultMessage: 'Agents',
|
defaultMessage: 'Agents',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -193,12 +182,12 @@ const breadcrumbGetters: {
|
||||||
BASE_BREADCRUMB,
|
BASE_BREADCRUMB,
|
||||||
{
|
{
|
||||||
href: pagePathGetters.fleet(),
|
href: pagePathGetters.fleet(),
|
||||||
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', {
|
text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', {
|
||||||
defaultMessage: 'Fleet',
|
defaultMessage: 'Agents',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle', {
|
text: i18n.translate('xpack.ingestManager.breadcrumbs.enrollmentTokensPageTitle', {
|
||||||
defaultMessage: 'Enrollment tokens',
|
defaultMessage: 'Enrollment tokens',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
|
@ -83,8 +83,8 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
|
||||||
disabled={!fleet?.enabled}
|
disabled={!fleet?.enabled}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.ingestManager.appNavigation.fleetLinkText"
|
id="xpack.ingestManager.appNavigation.agentsLinkText"
|
||||||
defaultMessage="Fleet"
|
defaultMessage="Agents"
|
||||||
/>
|
/>
|
||||||
</EuiTab>
|
</EuiTab>
|
||||||
<EuiTab isSelected={section === 'data_stream'} href={getHref('data_streams')}>
|
<EuiTab isSelected={section === 'data_stream'} href={getHref('data_streams')}>
|
||||||
|
|
|
@ -74,14 +74,14 @@ export const ManagedInstructions = React.memo<Props>(({ agentPolicies }) => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.ingestManager.agentEnrollment.fleetNotInitializedText"
|
id="xpack.ingestManager.agentEnrollment.agentsNotInitializedText"
|
||||||
defaultMessage="Before enrolling agents, {link}."
|
defaultMessage="Before enrolling agents, {link}."
|
||||||
values={{
|
values={{
|
||||||
link: (
|
link: (
|
||||||
<EuiLink href={getHref('fleet')}>
|
<EuiLink href={getHref('fleet')}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.ingestManager.agentEnrollment.setUpFleetLink"
|
id="xpack.ingestManager.agentEnrollment.setUpAgentsLink"
|
||||||
defaultMessage="set up Fleet"
|
defaultMessage="set up Agents"
|
||||||
/>
|
/>
|
||||||
</EuiLink>
|
</EuiLink>
|
||||||
),
|
),
|
||||||
|
|
|
@ -126,7 +126,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiText>
|
<EuiText>
|
||||||
<h1>
|
<h1>
|
||||||
<FormattedMessage id="xpack.ingestManager.fleet.pageTitle" defaultMessage="Fleet" />
|
<FormattedMessage id="xpack.ingestManager.agents.pageTitle" defaultMessage="Agents" />
|
||||||
</h1>
|
</h1>
|
||||||
</EuiText>
|
</EuiText>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
@ -134,7 +134,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {
|
||||||
<EuiText color="subdued">
|
<EuiText color="subdued">
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<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."
|
defaultMessage="Manage and deploy policy updates to a group of agents of any size."
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -25,8 +25,8 @@ export const OverviewAgentSection = () => {
|
||||||
return (
|
return (
|
||||||
<EuiFlexItem component="section">
|
<EuiFlexItem component="section">
|
||||||
<OverviewPanel
|
<OverviewPanel
|
||||||
title={i18n.translate('xpack.ingestManager.overviewPageFleetPanelTitle', {
|
title={i18n.translate('xpack.ingestManager.overviewPageAgentsPanelTitle', {
|
||||||
defaultMessage: 'Fleet',
|
defaultMessage: 'Agents',
|
||||||
})}
|
})}
|
||||||
tooltip={i18n.translate('xpack.ingestManager.overviewPageFleetPanelTooltip', {
|
tooltip={i18n.translate('xpack.ingestManager.overviewPageFleetPanelTooltip', {
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
|
|
|
@ -47,7 +47,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => {
|
||||||
<h1>
|
<h1>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.ingestManager.overviewPageTitle"
|
id="xpack.ingestManager.overviewPageTitle"
|
||||||
defaultMessage="Ingest Manager"
|
defaultMessage="Fleet"
|
||||||
/>
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
|
|
|
@ -78,7 +78,7 @@ export class IngestManagerPlugin
|
||||||
core.application.register({
|
core.application.register({
|
||||||
id: PLUGIN_ID,
|
id: PLUGIN_ID,
|
||||||
category: DEFAULT_APP_CATEGORIES.management,
|
category: DEFAULT_APP_CATEGORIES.management,
|
||||||
title: i18n.translate('xpack.ingestManager.appTitle', { defaultMessage: 'Ingest Manager' }),
|
title: i18n.translate('xpack.ingestManager.appTitle', { defaultMessage: 'Fleet' }),
|
||||||
order: 9020,
|
order: 9020,
|
||||||
euiIconType: 'logoElastic',
|
euiIconType: 'logoElastic',
|
||||||
async mount(params: AppMountParameters) {
|
async mount(params: AppMountParameters) {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { SavedObjectsClientContract } from 'kibana/server';
|
||||||
import { saveInstalledEsRefs } from '../../packages/install';
|
import { saveInstalledEsRefs } from '../../packages/install';
|
||||||
import * as Registry from '../../registry';
|
import * as Registry from '../../registry';
|
||||||
import {
|
import {
|
||||||
Dataset,
|
|
||||||
ElasticsearchAssetType,
|
ElasticsearchAssetType,
|
||||||
EsAssetReference,
|
EsAssetReference,
|
||||||
RegistryPackage,
|
RegistryPackage,
|
||||||
|
@ -24,12 +23,7 @@ interface TransformInstallation {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TransformPathDataset {
|
export const installTransform = async (
|
||||||
path: string;
|
|
||||||
dataset: Dataset;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const installTransformForDataset = async (
|
|
||||||
registryPackage: RegistryPackage,
|
registryPackage: RegistryPackage,
|
||||||
paths: string[],
|
paths: string[],
|
||||||
callCluster: CallESAsCurrentUser,
|
callCluster: CallESAsCurrentUser,
|
||||||
|
@ -51,53 +45,32 @@ export const installTransformForDataset = async (
|
||||||
callCluster,
|
callCluster,
|
||||||
previousInstalledTransformEsAssets.map((asset) => asset.id)
|
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));
|
const transformPaths = paths.filter((path) => isTransform(path));
|
||||||
let installedTransforms: EsAssetReference[] = [];
|
let installedTransforms: EsAssetReference[] = [];
|
||||||
if (transformPaths.length > 0) {
|
if (transformPaths.length > 0) {
|
||||||
const transformPathDatasets = datasets.reduce<TransformPathDataset[]>((acc, dataset) => {
|
const transformRefs = transformPaths.reduce<EsAssetReference[]>((acc, path) => {
|
||||||
transformPaths.forEach((path) => {
|
|
||||||
if (isDatasetTransform(path, dataset.path)) {
|
|
||||||
acc.push({ path, dataset });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const transformRefs = transformPathDatasets.reduce<EsAssetReference[]>(
|
|
||||||
(acc, transformPathDataset) => {
|
|
||||||
if (transformPathDataset) {
|
|
||||||
acc.push({
|
acc.push({
|
||||||
id: getTransformNameForInstallation(transformPathDataset, installNameSuffix),
|
id: getTransformNameForInstallation(registryPackage, path, installNameSuffix),
|
||||||
type: ElasticsearchAssetType.transform,
|
type: ElasticsearchAssetType.transform,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
}, []);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// get and save transform refs before installing transforms
|
// get and save transform refs before installing transforms
|
||||||
await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs);
|
await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs);
|
||||||
|
|
||||||
const transforms: TransformInstallation[] = transformPathDatasets.map(
|
const transforms: TransformInstallation[] = transformPaths.map((path: string) => {
|
||||||
(transformPathDataset: TransformPathDataset) => {
|
|
||||||
return {
|
return {
|
||||||
installationName: getTransformNameForInstallation(
|
installationName: getTransformNameForInstallation(registryPackage, path, installNameSuffix),
|
||||||
transformPathDataset,
|
content: getAsset(path).toString('utf-8'),
|
||||||
installNameSuffix
|
|
||||||
),
|
|
||||||
content: getAsset(transformPathDataset.path).toString('utf-8'),
|
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const installationPromises = transforms.map(async (transform) => {
|
const installationPromises = transforms.map(async (transform) => {
|
||||||
return installTransform({ callCluster, transform });
|
return handleTransformInstall({ callCluster, transform });
|
||||||
});
|
});
|
||||||
|
|
||||||
installedTransforms = await Promise.all(installationPromises).then((results) => results.flat());
|
installedTransforms = await Promise.all(installationPromises).then((results) => results.flat());
|
||||||
|
@ -123,20 +96,10 @@ export const installTransformForDataset = async (
|
||||||
|
|
||||||
const isTransform = (path: string) => {
|
const isTransform = (path: string) => {
|
||||||
const pathParts = Registry.pathParts(path);
|
const pathParts = Registry.pathParts(path);
|
||||||
return pathParts.type === ElasticsearchAssetType.transform;
|
return !path.endsWith('/') && pathParts.type === ElasticsearchAssetType.transform;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDatasetTransform = (path: string, datasetName: string) => {
|
async function handleTransformInstall({
|
||||||
const pathParts = Registry.pathParts(path);
|
|
||||||
return (
|
|
||||||
!path.endsWith('/') &&
|
|
||||||
pathParts.type === ElasticsearchAssetType.transform &&
|
|
||||||
pathParts.dataset !== undefined &&
|
|
||||||
datasetName === pathParts.dataset
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function installTransform({
|
|
||||||
callCluster,
|
callCluster,
|
||||||
transform,
|
transform,
|
||||||
}: {
|
}: {
|
||||||
|
@ -160,9 +123,12 @@ async function installTransform({
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTransformNameForInstallation = (
|
const getTransformNameForInstallation = (
|
||||||
transformDataset: TransformPathDataset,
|
registryPackage: RegistryPackage,
|
||||||
|
path: string,
|
||||||
suffix: string
|
suffix: string
|
||||||
) => {
|
) => {
|
||||||
const filename = transformDataset?.path.split('/')?.pop()?.split('.')[0];
|
const pathPaths = path.split('/');
|
||||||
return `${transformDataset.dataset.type}-${transformDataset.dataset.name}-${filename}-${suffix}`;
|
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(
|
await Promise.all(
|
||||||
transformIds.map(async (transformId) => {
|
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 stopTransforms([transformId], callCluster);
|
||||||
await callCluster('transport.request', {
|
await callCluster('transport.request', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
@ -32,6 +45,15 @@ export const deleteTransforms = async (
|
||||||
path: `/_transform/${transformId}`,
|
path: `/_transform/${transformId}`,
|
||||||
ignore: [404],
|
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 { ILegacyScopedClusterClient, SavedObject, SavedObjectsClientContract } from 'kibana/server';
|
||||||
import { ElasticsearchAssetType, Installation, RegistryPackage } from '../../../../types';
|
import { ElasticsearchAssetType, Installation, RegistryPackage } from '../../../../types';
|
||||||
import { getInstallation, getInstallationObject } from '../../packages';
|
import { getInstallation, getInstallationObject } from '../../packages';
|
||||||
|
@ -47,7 +47,7 @@ describe('test transform install', () => {
|
||||||
type: ElasticsearchAssetType.ingestPipeline,
|
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,
|
type: ElasticsearchAssetType.transform,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -60,15 +60,15 @@ describe('test transform install', () => {
|
||||||
type: ElasticsearchAssetType.ingestPipeline,
|
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,
|
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,
|
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,
|
type: ElasticsearchAssetType.transform,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -91,7 +91,26 @@ describe('test transform install', () => {
|
||||||
} as unknown) as SavedObject<Installation>)
|
} 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',
|
name: 'endpoint',
|
||||||
version: '0.16.0-dev.0',
|
version: '0.16.0-dev.0',
|
||||||
|
@ -128,18 +147,26 @@ describe('test transform install', () => {
|
||||||
} as unknown) as RegistryPackage,
|
} as unknown) as RegistryPackage,
|
||||||
[
|
[
|
||||||
'endpoint-0.16.0-dev.0/dataset/policy/elasticsearch/ingest_pipeline/default.json',
|
'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/elasticsearch/transform/metadata/default.json',
|
||||||
'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,
|
legacyScopedClusterClient.callAsCurrentUser,
|
||||||
savedObjectsClient
|
savedObjectsClient
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
|
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
'transport.request',
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
path: '/_transform/endpoint.metadata_current-default-0.15.0-dev.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'transport.request',
|
'transport.request',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
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',
|
query: 'force=true',
|
||||||
ignore: [404],
|
ignore: [404],
|
||||||
},
|
},
|
||||||
|
@ -149,7 +176,15 @@ describe('test transform install', () => {
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
query: 'force=true',
|
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],
|
ignore: [404],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -157,7 +192,7 @@ describe('test transform install', () => {
|
||||||
'transport.request',
|
'transport.request',
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
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',
|
query: 'defer_validation=true',
|
||||||
body: '{"content": "data"}',
|
body: '{"content": "data"}',
|
||||||
},
|
},
|
||||||
|
@ -166,7 +201,7 @@ describe('test transform install', () => {
|
||||||
'transport.request',
|
'transport.request',
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
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',
|
query: 'defer_validation=true',
|
||||||
body: '{"content": "data"}',
|
body: '{"content": "data"}',
|
||||||
},
|
},
|
||||||
|
@ -175,14 +210,14 @@ describe('test transform install', () => {
|
||||||
'transport.request',
|
'transport.request',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
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',
|
'transport.request',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
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',
|
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',
|
type: 'transform',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
|
id: 'endpoint.metadata-default-0.16.0-dev.0',
|
||||||
type: 'transform',
|
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',
|
type: 'transform',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -222,11 +257,11 @@ describe('test transform install', () => {
|
||||||
type: 'ingest_pipeline',
|
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',
|
type: 'transform',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
|
id: 'endpoint.metadata-default-0.16.0-dev.0',
|
||||||
type: 'transform',
|
type: 'transform',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -263,7 +298,7 @@ describe('test transform install', () => {
|
||||||
>)
|
>)
|
||||||
);
|
);
|
||||||
legacyScopedClusterClient.callAsCurrentUser = jest.fn();
|
legacyScopedClusterClient.callAsCurrentUser = jest.fn();
|
||||||
await installTransformForDataset(
|
await installTransform(
|
||||||
({
|
({
|
||||||
name: 'endpoint',
|
name: 'endpoint',
|
||||||
version: '0.16.0-dev.0',
|
version: '0.16.0-dev.0',
|
||||||
|
@ -284,7 +319,7 @@ describe('test transform install', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as unknown) as RegistryPackage,
|
} 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,
|
legacyScopedClusterClient.callAsCurrentUser,
|
||||||
savedObjectsClient
|
savedObjectsClient
|
||||||
);
|
);
|
||||||
|
@ -294,7 +329,7 @@ describe('test transform install', () => {
|
||||||
'transport.request',
|
'transport.request',
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
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',
|
query: 'defer_validation=true',
|
||||||
body: '{"content": "data"}',
|
body: '{"content": "data"}',
|
||||||
},
|
},
|
||||||
|
@ -303,7 +338,7 @@ describe('test transform install', () => {
|
||||||
'transport.request',
|
'transport.request',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
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',
|
'endpoint',
|
||||||
{
|
{
|
||||||
installed_es: [
|
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 = ({
|
const previousInstallation: Installation = ({
|
||||||
installed_es: [
|
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,
|
type: ElasticsearchAssetType.transform,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -346,7 +381,26 @@ describe('test transform install', () => {
|
||||||
} as unknown) as SavedObject<Installation>)
|
} 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',
|
name: 'endpoint',
|
||||||
version: '0.16.0-dev.0',
|
version: '0.16.0-dev.0',
|
||||||
|
@ -387,11 +441,18 @@ describe('test transform install', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
|
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
'transport.request',
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
path: '/_transform/endpoint.metadata-current-default-0.15.0-dev.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'transport.request',
|
'transport.request',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
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',
|
query: 'force=true',
|
||||||
ignore: [404],
|
ignore: [404],
|
||||||
},
|
},
|
||||||
|
@ -401,7 +462,15 @@ describe('test transform install', () => {
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
query: 'force=true',
|
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],
|
ignore: [404],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {
|
||||||
ElasticsearchAssetType,
|
ElasticsearchAssetType,
|
||||||
InstallType,
|
InstallType,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import { appContextService } from '../../index';
|
|
||||||
import { installIndexPatterns } from '../kibana/index_pattern/install';
|
import { installIndexPatterns } from '../kibana/index_pattern/install';
|
||||||
import * as Registry from '../registry';
|
import * as Registry from '../registry';
|
||||||
import {
|
import {
|
||||||
|
@ -45,7 +44,8 @@ import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
|
||||||
import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove';
|
import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove';
|
||||||
import { IngestManagerError, PackageOutdatedError } from '../../../errors';
|
import { IngestManagerError, PackageOutdatedError } from '../../../errors';
|
||||||
import { getPackageSavedObjects } from './get';
|
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: {
|
export async function installLatestPackage(options: {
|
||||||
savedObjectsClient: SavedObjectsClientContract;
|
savedObjectsClient: SavedObjectsClientContract;
|
||||||
|
@ -325,7 +325,7 @@ export async function installPackage({
|
||||||
// update current backing indices of each data stream
|
// update current backing indices of each data stream
|
||||||
await updateCurrentWriteIndices(callCluster, installedTemplates);
|
await updateCurrentWriteIndices(callCluster, installedTemplates);
|
||||||
|
|
||||||
const installedTransforms = await installTransformForDataset(
|
const installedTransforms = await installTransform(
|
||||||
registryPackageInfo,
|
registryPackageInfo,
|
||||||
paths,
|
paths,
|
||||||
callCluster,
|
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) {
|
async clickProcessor(processorSelector: string) {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
find(`${processorSelector}.manageItemButton`).simulate('click');
|
find(`${processorSelector}.manageItemButton`).simulate('click');
|
||||||
|
@ -230,6 +265,7 @@ type TestSubject =
|
||||||
| 'addDocumentsButton'
|
| 'addDocumentsButton'
|
||||||
| 'testPipelineFlyout'
|
| 'testPipelineFlyout'
|
||||||
| 'documentsDropdown'
|
| 'documentsDropdown'
|
||||||
|
| 'documentsDropdown.documentsButton'
|
||||||
| 'outputTab'
|
| 'outputTab'
|
||||||
| 'documentsEditor'
|
| 'documentsEditor'
|
||||||
| 'runPipelineButton'
|
| 'runPipelineButton'
|
||||||
|
@ -248,6 +284,8 @@ type TestSubject =
|
||||||
| 'configurationTab'
|
| 'configurationTab'
|
||||||
| 'outputTab'
|
| 'outputTab'
|
||||||
| 'processorOutputTabContent'
|
| 'processorOutputTabContent'
|
||||||
|
| 'editDocumentsButton'
|
||||||
|
| 'clearAllDocumentsButton'
|
||||||
| 'addDocumentsAccordion'
|
| 'addDocumentsAccordion'
|
||||||
| 'addDocumentButton'
|
| 'addDocumentButton'
|
||||||
| 'addDocumentError'
|
| 'addDocumentError'
|
||||||
|
|
|
@ -22,6 +22,27 @@ describe('Test pipeline', () => {
|
||||||
|
|
||||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
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(() => {
|
beforeAll(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
@ -236,30 +257,77 @@ describe('Test pipeline', () => {
|
||||||
expect(find('addDocumentError').text()).toContain(error.message);
|
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', () => {
|
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 () => {
|
it('should show "inactive" processor status by default', async () => {
|
||||||
const { find } = testBed;
|
const { find } = testBed;
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ export const EditProcessorForm: FunctionComponent<Props> = ({
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
resetProcessors,
|
resetProcessors,
|
||||||
}) => {
|
}) => {
|
||||||
const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext();
|
const { testPipelineData, testPipelineDataDispatch } = useTestPipelineContext();
|
||||||
const {
|
const {
|
||||||
testOutputPerProcessor,
|
testOutputPerProcessor,
|
||||||
config: { selectedDocumentIndex, documents },
|
config: { selectedDocumentIndex, documents },
|
||||||
|
@ -117,7 +117,7 @@ export const EditProcessorForm: FunctionComponent<Props> = ({
|
||||||
testOutputPerProcessor[selectedDocumentIndex][processor.id];
|
testOutputPerProcessor[selectedDocumentIndex][processor.id];
|
||||||
|
|
||||||
const updateSelectedDocument = (index: number) => {
|
const updateSelectedDocument = (index: number) => {
|
||||||
setCurrentTestPipelineData({
|
testPipelineDataDispatch({
|
||||||
type: 'updateActiveDocument',
|
type: 'updateActiveDocument',
|
||||||
payload: {
|
payload: {
|
||||||
config: {
|
config: {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
import { EuiButtonEmpty } from '@elastic/eui';
|
import { EuiButtonEmpty } from '@elastic/eui';
|
||||||
import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs';
|
import { TestPipelineFlyoutTab } from './test_pipeline_tabs';
|
||||||
|
|
||||||
const i18nTexts = {
|
const i18nTexts = {
|
||||||
buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel', {
|
buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel', {
|
||||||
|
|
|
@ -8,18 +8,15 @@ import React, { FunctionComponent, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiPopover,
|
EuiPopover,
|
||||||
|
EuiPopoverFooter,
|
||||||
EuiButtonEmpty,
|
EuiButtonEmpty,
|
||||||
EuiPopoverTitle,
|
EuiPopoverTitle,
|
||||||
EuiSelectable,
|
EuiSelectable,
|
||||||
EuiHorizontalRule,
|
|
||||||
EuiFlexGroup,
|
|
||||||
EuiFlexItem,
|
|
||||||
EuiSpacer,
|
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import { Document } from '../../../types';
|
import { Document } from '../../../types';
|
||||||
|
|
||||||
import { TestPipelineFlyoutTab } from '../test_pipeline_flyout_tabs';
|
import { TestPipelineFlyoutTab } from '../test_pipeline_tabs';
|
||||||
|
|
||||||
import './documents_dropdown.scss';
|
import './documents_dropdown.scss';
|
||||||
|
|
||||||
|
@ -31,9 +28,9 @@ const i18nTexts = {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
addDocumentsButtonLabel: i18n.translate(
|
addDocumentsButtonLabel: i18n.translate(
|
||||||
'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.buttonLabel',
|
'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.editDocumentsButtonLabel',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Add documents',
|
defaultMessage: 'Edit documents',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
popoverTitle: i18n.translate(
|
popoverTitle: i18n.translate(
|
||||||
|
@ -88,8 +85,10 @@ export const DocumentsDropdown: FunctionComponent<Props> = ({
|
||||||
>
|
>
|
||||||
<EuiSelectable
|
<EuiSelectable
|
||||||
singleSelection
|
singleSelection
|
||||||
|
data-test-subj="documentList"
|
||||||
options={documents.map((doc, index) => ({
|
options={documents.map((doc, index) => ({
|
||||||
key: index.toString(),
|
key: index.toString(),
|
||||||
|
'data-test-subj': 'documentListItem',
|
||||||
checked: selectedDocumentIndex === index ? 'on' : undefined,
|
checked: selectedDocumentIndex === index ? 'on' : undefined,
|
||||||
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.documentLabel', {
|
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.documentLabel', {
|
||||||
defaultMessage: 'Document {documentNumber}',
|
defaultMessage: 'Document {documentNumber}',
|
||||||
|
@ -107,32 +106,27 @@ export const DocumentsDropdown: FunctionComponent<Props> = ({
|
||||||
setShowPopover(false);
|
setShowPopover(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(list, search) => (
|
{(list) => (
|
||||||
<div>
|
<>
|
||||||
<EuiPopoverTitle>{i18nTexts.popoverTitle}</EuiPopoverTitle>
|
<EuiPopoverTitle>{i18nTexts.popoverTitle}</EuiPopoverTitle>
|
||||||
{list}
|
{list}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</EuiSelectable>
|
</EuiSelectable>
|
||||||
|
|
||||||
<EuiHorizontalRule margin="xs" />
|
<EuiPopoverFooter>
|
||||||
|
|
||||||
<EuiFlexGroup justifyContent="center">
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiButton
|
<EuiButton
|
||||||
size="s"
|
size="s"
|
||||||
|
fullWidth
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openFlyout('documents');
|
openFlyout('documents');
|
||||||
setShowPopover(false);
|
setShowPopover(false);
|
||||||
}}
|
}}
|
||||||
data-test-subj="addDocumentsButton"
|
data-test-subj="editDocumentsButton"
|
||||||
>
|
>
|
||||||
{i18nTexts.addDocumentsButtonLabel}
|
{i18nTexts.addDocumentsButtonLabel}
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
</EuiFlexItem>
|
</EuiPopoverFooter>
|
||||||
</EuiFlexGroup>
|
|
||||||
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
</EuiPopover>
|
</EuiPopover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
import { EuiButton } from '@elastic/eui';
|
import { EuiButton } from '@elastic/eui';
|
||||||
import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs';
|
import { TestPipelineFlyoutTab } from './test_pipeline_tabs';
|
||||||
|
|
||||||
const i18nTexts = {
|
const i18nTexts = {
|
||||||
buttonLabel: i18n.translate(
|
buttonLabel: i18n.translate(
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||||
|
|
||||||
import { useTestPipelineContext, usePipelineProcessorsContext } from '../../context';
|
import { useTestPipelineContext, usePipelineProcessorsContext } from '../../context';
|
||||||
import { DocumentsDropdown } from './documents_dropdown';
|
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 { AddDocumentsButton } from './add_documents_button';
|
||||||
import { TestOutputButton } from './test_output_button';
|
import { TestOutputButton } from './test_output_button';
|
||||||
import { TestPipelineFlyout } from './test_pipeline_flyout.container';
|
import { TestPipelineFlyout } from './test_pipeline_flyout.container';
|
||||||
|
@ -24,7 +24,7 @@ const i18nTexts = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TestPipelineActions: FunctionComponent = () => {
|
export const TestPipelineActions: FunctionComponent = () => {
|
||||||
const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext();
|
const { testPipelineData, testPipelineDataDispatch } = useTestPipelineContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: { processors },
|
state: { processors },
|
||||||
|
@ -39,7 +39,7 @@ export const TestPipelineActions: FunctionComponent = () => {
|
||||||
const [activeFlyoutTab, setActiveFlyoutTab] = useState<TestPipelineFlyoutTab>('documents');
|
const [activeFlyoutTab, setActiveFlyoutTab] = useState<TestPipelineFlyoutTab>('documents');
|
||||||
|
|
||||||
const updateSelectedDocument = (index: number) => {
|
const updateSelectedDocument = (index: number) => {
|
||||||
setCurrentTestPipelineData({
|
testPipelineDataDispatch({
|
||||||
type: 'updateActiveDocument',
|
type: 'updateActiveDocument',
|
||||||
payload: {
|
payload: {
|
||||||
config: {
|
config: {
|
||||||
|
|
|
@ -15,8 +15,7 @@ import { Document } from '../../types';
|
||||||
import { useIsMounted } from '../../use_is_mounted';
|
import { useIsMounted } from '../../use_is_mounted';
|
||||||
import { TestPipelineFlyout as ViewComponent } from './test_pipeline_flyout';
|
import { TestPipelineFlyout as ViewComponent } from './test_pipeline_flyout';
|
||||||
|
|
||||||
import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs';
|
import { TestPipelineFlyoutTab } from './test_pipeline_tabs';
|
||||||
import { documentsSchema } from './test_pipeline_flyout_tabs/documents_schema';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
activeTab: TestPipelineFlyoutTab;
|
activeTab: TestPipelineFlyoutTab;
|
||||||
|
@ -39,7 +38,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
|
|
||||||
const {
|
const {
|
||||||
testPipelineData,
|
testPipelineData,
|
||||||
setCurrentTestPipelineData,
|
testPipelineDataDispatch,
|
||||||
updateTestOutputPerProcessor,
|
updateTestOutputPerProcessor,
|
||||||
} = useTestPipelineContext();
|
} = useTestPipelineContext();
|
||||||
|
|
||||||
|
@ -48,7 +47,6 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
} = testPipelineData;
|
} = testPipelineData;
|
||||||
|
|
||||||
const { form } = useForm({
|
const { form } = useForm({
|
||||||
schema: documentsSchema,
|
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
documents: cachedDocuments || '',
|
documents: cachedDocuments || '',
|
||||||
},
|
},
|
||||||
|
@ -88,7 +86,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
// reset the per-processor output
|
// reset the per-processor output
|
||||||
// this is needed in the scenario where the pipeline has already executed,
|
// 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
|
// but you modified the sample documents and there was an error on re-execution
|
||||||
setCurrentTestPipelineData({
|
testPipelineDataDispatch({
|
||||||
type: 'updateOutputPerProcessor',
|
type: 'updateOutputPerProcessor',
|
||||||
payload: {
|
payload: {
|
||||||
isExecutingPipeline: false,
|
isExecutingPipeline: false,
|
||||||
|
@ -99,7 +97,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
return { isSuccessful: false };
|
return { isSuccessful: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentTestPipelineData({
|
testPipelineDataDispatch({
|
||||||
type: 'updateConfig',
|
type: 'updateConfig',
|
||||||
payload: {
|
payload: {
|
||||||
config: {
|
config: {
|
||||||
|
@ -133,7 +131,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
processors,
|
processors,
|
||||||
services.api,
|
services.api,
|
||||||
services.notifications.toasts,
|
services.notifications.toasts,
|
||||||
setCurrentTestPipelineData,
|
testPipelineDataDispatch,
|
||||||
updateTestOutputPerProcessor,
|
updateTestOutputPerProcessor,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -157,6 +155,12 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetTestOutput = () => {
|
||||||
|
testPipelineDataDispatch({
|
||||||
|
type: 'reset',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cachedDocuments && activeTab === 'output') {
|
if (cachedDocuments && activeTab === 'output') {
|
||||||
handleTestPipeline({ documents: cachedDocuments, verbose: cachedVerbose }, true);
|
handleTestPipeline({ documents: cachedDocuments, verbose: cachedVerbose }, true);
|
||||||
|
@ -169,6 +173,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
return (
|
return (
|
||||||
<ViewComponent
|
<ViewComponent
|
||||||
handleTestPipeline={handleTestPipeline}
|
handleTestPipeline={handleTestPipeline}
|
||||||
|
resetTestOutput={resetTestOutput}
|
||||||
isRunningTest={isRunningTest}
|
isRunningTest={isRunningTest}
|
||||||
cachedVerbose={cachedVerbose}
|
cachedVerbose={cachedVerbose}
|
||||||
cachedDocuments={cachedDocuments}
|
cachedDocuments={cachedDocuments}
|
||||||
|
|
|
@ -19,8 +19,7 @@ import {
|
||||||
import { FormHook } from '../../../../../shared_imports';
|
import { FormHook } from '../../../../../shared_imports';
|
||||||
import { Document } from '../../types';
|
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 {
|
export interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
handleTestPipeline: (
|
handleTestPipeline: (
|
||||||
|
@ -31,11 +30,14 @@ export interface Props {
|
||||||
cachedVerbose?: boolean;
|
cachedVerbose?: boolean;
|
||||||
cachedDocuments?: Document[];
|
cachedDocuments?: Document[];
|
||||||
testOutput?: any;
|
testOutput?: any;
|
||||||
form: FormHook;
|
form: FormHook<{
|
||||||
|
documents: string | Document[];
|
||||||
|
}>;
|
||||||
validateAndTestPipeline: () => Promise<void>;
|
validateAndTestPipeline: () => Promise<void>;
|
||||||
selectedTab: TestPipelineFlyoutTab;
|
selectedTab: TestPipelineFlyoutTab;
|
||||||
setSelectedTab: (selectedTa: TestPipelineFlyoutTab) => void;
|
setSelectedTab: (selectedTa: TestPipelineFlyoutTab) => void;
|
||||||
testingError: any;
|
testingError: any;
|
||||||
|
resetTestOutput: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestPipelineConfig {
|
export interface TestPipelineConfig {
|
||||||
|
@ -45,6 +47,7 @@ export interface TestPipelineConfig {
|
||||||
|
|
||||||
export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
handleTestPipeline,
|
handleTestPipeline,
|
||||||
|
resetTestOutput,
|
||||||
isRunningTest,
|
isRunningTest,
|
||||||
cachedVerbose,
|
cachedVerbose,
|
||||||
cachedDocuments,
|
cachedDocuments,
|
||||||
|
@ -75,6 +78,7 @@ export const TestPipelineFlyout: React.FunctionComponent<Props> = ({
|
||||||
form={form}
|
form={form}
|
||||||
validateAndTestPipeline={validateAndTestPipeline}
|
validateAndTestPipeline={validateAndTestPipeline}
|
||||||
isRunningTest={isRunningTest}
|
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,
|
TextField,
|
||||||
fieldValidators,
|
fieldValidators,
|
||||||
FieldConfig,
|
FieldConfig,
|
||||||
} from '../../../../../../shared_imports';
|
} from '../../../../../../../shared_imports';
|
||||||
import { useIsMounted } from '../../../use_is_mounted';
|
import { useIsMounted } from '../../../../use_is_mounted';
|
||||||
import { Document } from '../../../types';
|
import { Document } from '../../../../types';
|
||||||
|
|
||||||
const UseField = getUseField({ component: Field });
|
const UseField = getUseField({ component: Field });
|
||||||
|
|
|
@ -11,8 +11,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { EuiAccordion, EuiText, EuiSpacer, EuiLink } from '@elastic/eui';
|
import { EuiAccordion, EuiText, EuiSpacer, EuiLink } from '@elastic/eui';
|
||||||
import { UrlGeneratorsDefinition } from 'src/plugins/share/public';
|
import { UrlGeneratorsDefinition } from 'src/plugins/share/public';
|
||||||
|
|
||||||
import { useKibana } from '../../../../../../../shared_imports';
|
import { useKibana } from '../../../../../../../../shared_imports';
|
||||||
import { useIsMounted } from '../../../../use_is_mounted';
|
import { useIsMounted } from '../../../../../use_is_mounted';
|
||||||
import { AddDocumentForm } from '../add_document_form';
|
import { AddDocumentForm } from '../add_document_form';
|
||||||
|
|
||||||
import './add_documents_accordion.scss';
|
import './add_documents_accordion.scss';
|
||||||
|
@ -26,6 +26,12 @@ const i18nTexts = {
|
||||||
defaultMessage: 'Add documents from index',
|
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 {
|
interface Props {
|
||||||
|
@ -79,10 +85,7 @@ export const AddDocumentsAccordion: FunctionComponent<Props> = ({ onAddDocuments
|
||||||
<div className="addDocumentsAccordion">
|
<div className="addDocumentsAccordion">
|
||||||
<EuiText size="s" color="subdued">
|
<EuiText size="s" color="subdued">
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
{i18nTexts.addDocumentsDescription}
|
||||||
id="xpack.ingestPipelines.pipelineEditor.addDocumentsAccordion.contentDescriptionText"
|
|
||||||
defaultMessage="Provide the index name and document ID of the indexed document to test."
|
|
||||||
/>
|
|
||||||
{discoverLink && (
|
{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';
|
type: 'updateIsExecutingPipeline';
|
||||||
payload: Pick<TestPipelineData, 'isExecutingPipeline'>;
|
payload: Pick<TestPipelineData, 'isExecutingPipeline'>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'reset';
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TestPipelineContext {
|
export interface TestPipelineContext {
|
||||||
testPipelineData: TestPipelineData;
|
testPipelineData: TestPipelineData;
|
||||||
setCurrentTestPipelineData: (data: Action) => void;
|
testPipelineDataDispatch: (data: Action) => void;
|
||||||
updateTestOutputPerProcessor: (
|
updateTestOutputPerProcessor: (
|
||||||
documents: Document[] | undefined,
|
documents: Document[] | undefined,
|
||||||
processors: DeserializeResult
|
processors: DeserializeResult
|
||||||
|
@ -69,7 +72,7 @@ const DEFAULT_TEST_PIPELINE_CONTEXT = {
|
||||||
},
|
},
|
||||||
isExecutingPipeline: false,
|
isExecutingPipeline: false,
|
||||||
},
|
},
|
||||||
setCurrentTestPipelineData: () => {},
|
testPipelineDataDispatch: () => {},
|
||||||
updateTestOutputPerProcessor: () => {},
|
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;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -193,7 +200,7 @@ export const TestPipelineContextProvider = ({ children }: { children: React.Reac
|
||||||
<TestPipelineContext.Provider
|
<TestPipelineContext.Provider
|
||||||
value={{
|
value={{
|
||||||
testPipelineData: state,
|
testPipelineData: state,
|
||||||
setCurrentTestPipelineData: dispatch,
|
testPipelineDataDispatch: dispatch,
|
||||||
updateTestOutputPerProcessor,
|
updateTestOutputPerProcessor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
export const eventsIndexPattern = 'logs-endpoint.events.*';
|
export const eventsIndexPattern = 'logs-endpoint.events.*';
|
||||||
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
|
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
|
||||||
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
|
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
|
||||||
export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current-*';
|
export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current_*';
|
||||||
export const metadataTransformPrefix = 'metrics-endpoint.metadata-current-default';
|
export const metadataTransformPrefix = 'endpoint.metadata_current-default';
|
||||||
export const policyIndexPattern = 'metrics-endpoint.policy-*';
|
export const policyIndexPattern = 'metrics-endpoint.policy-*';
|
||||||
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
|
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
|
||||||
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
|
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
|
||||||
|
|
|
@ -158,6 +158,7 @@ describe('Alerts', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('Opening alerts', () => {
|
context('Opening alerts', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
esArchiverLoad('closed_alerts');
|
esArchiverLoad('closed_alerts');
|
||||||
|
@ -204,6 +205,7 @@ describe('Alerts', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('Marking alerts as in-progress', () => {
|
context('Marking alerts as in-progress', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
esArchiverLoad('alerts');
|
esArchiverLoad('alerts');
|
||||||
|
|
|
@ -43,6 +43,7 @@ describe('Alerts detection rules', () => {
|
||||||
waitForAlertsIndexToBeCreated();
|
waitForAlertsIndexToBeCreated();
|
||||||
goToManageAlertsDetectionRules();
|
goToManageAlertsDetectionRules();
|
||||||
waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded();
|
waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded();
|
||||||
|
|
||||||
cy.get(RULE_NAME)
|
cy.get(RULE_NAME)
|
||||||
.eq(FIFTH_RULE)
|
.eq(FIFTH_RULE)
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
|
@ -56,7 +57,6 @@ describe('Alerts detection rules', () => {
|
||||||
activateRule(SEVENTH_RULE);
|
activateRule(SEVENTH_RULE);
|
||||||
waitForRuleToBeActivated();
|
waitForRuleToBeActivated();
|
||||||
sortByActivatedRules();
|
sortByActivatedRules();
|
||||||
|
|
||||||
cy.get(RULE_NAME)
|
cy.get(RULE_NAME)
|
||||||
.eq(FIRST_RULE)
|
.eq(FIRST_RULE)
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
|
@ -70,7 +70,6 @@ describe('Alerts detection rules', () => {
|
||||||
cy.wrap(expectedRulesNames).should('include', seventhRuleName);
|
cy.wrap(expectedRulesNames).should('include', seventhRuleName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(RULE_SWITCH).eq(FIRST_RULE).should('have.attr', 'role', 'switch');
|
cy.get(RULE_SWITCH).eq(FIRST_RULE).should('have.attr', 'role', 'switch');
|
||||||
cy.get(RULE_SWITCH).eq(SECOND_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.
|
* 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 {
|
import {
|
||||||
CUSTOM_RULES_BTN,
|
CUSTOM_RULES_BTN,
|
||||||
|
@ -12,20 +20,49 @@ import {
|
||||||
RULE_NAME,
|
RULE_NAME,
|
||||||
RULES_ROW,
|
RULES_ROW,
|
||||||
RULES_TABLE,
|
RULES_TABLE,
|
||||||
|
RULE_SWITCH,
|
||||||
SEVERITY,
|
SEVERITY,
|
||||||
SHOWING_RULES_TEXT,
|
SHOWING_RULES_TEXT,
|
||||||
} from '../screens/alerts_detection_rules';
|
} from '../screens/alerts_detection_rules';
|
||||||
import {
|
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_INVESTIGATION_NOTES,
|
||||||
ABOUT_RULE_DESCRIPTION,
|
ABOUT_RULE_DESCRIPTION,
|
||||||
|
CUSTOM_QUERY_DETAILS,
|
||||||
|
DEFINITION_DETAILS,
|
||||||
|
FALSE_POSITIVES_DETAILS,
|
||||||
|
getDetails,
|
||||||
|
INDEX_PATTERNS_DETAILS,
|
||||||
INVESTIGATION_NOTES_MARKDOWN,
|
INVESTIGATION_NOTES_MARKDOWN,
|
||||||
INVESTIGATION_NOTES_TOGGLE,
|
INVESTIGATION_NOTES_TOGGLE,
|
||||||
|
MITRE_ATTACK_DETAILS,
|
||||||
|
REFERENCE_URLS_DETAILS,
|
||||||
|
RISK_SCORE_DETAILS,
|
||||||
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
|
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
|
||||||
RULE_NAME_HEADER,
|
RULE_NAME_HEADER,
|
||||||
getDescriptionForTitle,
|
RULE_TYPE_DETAILS,
|
||||||
ABOUT_DETAILS,
|
RUNS_EVERY_DETAILS,
|
||||||
DEFINITION_DETAILS,
|
|
||||||
SCHEDULE_DETAILS,
|
SCHEDULE_DETAILS,
|
||||||
|
SEVERITY_DETAILS,
|
||||||
|
TAGS_DETAILS,
|
||||||
|
TIMELINE_TEMPLATE_DETAILS,
|
||||||
} from '../screens/rule_details';
|
} from '../screens/rule_details';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -37,46 +74,46 @@ import {
|
||||||
changeToThreeHundredRowsPerPage,
|
changeToThreeHundredRowsPerPage,
|
||||||
deleteFirstRule,
|
deleteFirstRule,
|
||||||
deleteSelectedRules,
|
deleteSelectedRules,
|
||||||
|
editFirstRule,
|
||||||
filterByCustomRules,
|
filterByCustomRules,
|
||||||
goToCreateNewRule,
|
goToCreateNewRule,
|
||||||
goToRuleDetails,
|
goToRuleDetails,
|
||||||
selectNumberOfRules,
|
selectNumberOfRules,
|
||||||
waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded,
|
waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded,
|
||||||
waitForRulesToBeLoaded,
|
waitForRulesToBeLoaded,
|
||||||
editFirstRule,
|
|
||||||
} from '../tasks/alerts_detection_rules';
|
} from '../tasks/alerts_detection_rules';
|
||||||
import {
|
import {
|
||||||
createAndActivateRule,
|
createAndActivateRule,
|
||||||
|
fillAboutRule,
|
||||||
fillAboutRuleAndContinue,
|
fillAboutRuleAndContinue,
|
||||||
fillDefineCustomRuleWithImportedQueryAndContinue,
|
fillDefineCustomRuleWithImportedQueryAndContinue,
|
||||||
|
fillScheduleRuleAndContinue,
|
||||||
goToAboutStepTab,
|
goToAboutStepTab,
|
||||||
goToScheduleStepTab,
|
|
||||||
goToActionsStepTab,
|
goToActionsStepTab,
|
||||||
fillAboutRule,
|
goToScheduleStepTab,
|
||||||
|
waitForTheRuleToBeExecuted,
|
||||||
} from '../tasks/create_new_rule';
|
} from '../tasks/create_new_rule';
|
||||||
|
import { saveEditedRule } from '../tasks/edit_rule';
|
||||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||||
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
||||||
|
import { refreshPage } from '../tasks/security_header';
|
||||||
|
|
||||||
import { DETECTIONS_URL } from '../urls/navigation';
|
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(() => {
|
before(() => {
|
||||||
esArchiverLoad('timeline');
|
esArchiverLoad('timeline');
|
||||||
});
|
});
|
||||||
|
@ -85,7 +122,7 @@ describe('Detection rules, custom', () => {
|
||||||
esArchiverUnload('timeline');
|
esArchiverUnload('timeline');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Creates and activates a new custom rule', () => {
|
it('Creates and activates a new rule', () => {
|
||||||
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
|
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
|
||||||
waitForAlertsPanelToBeLoaded();
|
waitForAlertsPanelToBeLoaded();
|
||||||
waitForAlertsIndexToBeCreated();
|
waitForAlertsIndexToBeCreated();
|
||||||
|
@ -94,27 +131,27 @@ describe('Detection rules, custom', () => {
|
||||||
goToCreateNewRule();
|
goToCreateNewRule();
|
||||||
fillDefineCustomRuleWithImportedQueryAndContinue(newRule);
|
fillDefineCustomRuleWithImportedQueryAndContinue(newRule);
|
||||||
fillAboutRuleAndContinue(newRule);
|
fillAboutRuleAndContinue(newRule);
|
||||||
|
fillScheduleRuleAndContinue(newRule);
|
||||||
|
|
||||||
// expect define step to repopulate
|
// expect define step to repopulate
|
||||||
cy.get(DEFINE_EDIT_BUTTON).click();
|
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('exist').click({ force: true });
|
||||||
cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist');
|
cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist');
|
||||||
|
|
||||||
// expect about step to populate
|
// expect about step to populate
|
||||||
cy.get(ABOUT_EDIT_BUTTON).click();
|
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('exist').click({ force: true });
|
||||||
cy.get(ABOUT_CONTINUE_BTN).should('not.exist');
|
cy.get(ABOUT_CONTINUE_BTN).should('not.exist');
|
||||||
|
|
||||||
createAndActivateRule();
|
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();
|
changeToThreeHundredRowsPerPage();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
|
||||||
const expectedNumberOfRules = 1;
|
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
|
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.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
||||||
});
|
});
|
||||||
cy.get(RULE_NAME).invoke('text').should('eql', newRule.name);
|
cy.get(RULE_NAME).should('have.text', newRule.name);
|
||||||
cy.get(RISK_SCORE).invoke('text').should('eql', newRule.riskScore);
|
cy.get(RISK_SCORE).should('have.text', newRule.riskScore);
|
||||||
cy.get(SEVERITY).invoke('text').should('eql', newRule.severity);
|
cy.get(SEVERITY).should('have.text', newRule.severity);
|
||||||
cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true');
|
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||||
|
|
||||||
goToRuleDetails();
|
goToRuleDetails();
|
||||||
|
|
||||||
let expectedUrls = '';
|
cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name} Beta`);
|
||||||
newRule.referenceUrls.forEach((url) => {
|
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newRule.description);
|
||||||
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(ABOUT_DETAILS).within(() => {
|
cy.get(ABOUT_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Severity').invoke('text').should('eql', newRule.severity);
|
getDetails(SEVERITY_DETAILS).should('have.text', newRule.severity);
|
||||||
getDescriptionForTitle('Risk score').invoke('text').should('eql', newRule.riskScore);
|
getDetails(RISK_SCORE_DETAILS).should('have.text', newRule.riskScore);
|
||||||
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
|
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
|
||||||
getDescriptionForTitle('False positive examples')
|
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
|
||||||
.invoke('text')
|
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
|
||||||
.should('eql', expectedFalsePositives);
|
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
|
||||||
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
|
|
||||||
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
|
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(() => {
|
cy.get(DEFINITION_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Index patterns')
|
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
|
||||||
.invoke('text')
|
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newRule.customQuery} `);
|
||||||
.should('eql', expectedIndexPatterns.join(''));
|
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
|
||||||
getDescriptionForTitle('Custom query')
|
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||||
.invoke('text')
|
|
||||||
.should('eql', `${newRule.customQuery} `);
|
|
||||||
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query');
|
|
||||||
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(SCHEDULE_DETAILS).within(() => {
|
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
|
getDetails(RUNS_EVERY_DETAILS).should(
|
||||||
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
|
'have.text',
|
||||||
|
`${newRule.runsEvery.interval}${newRule.runsEvery.type}`
|
||||||
|
);
|
||||||
|
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
|
||||||
|
'have.text',
|
||||||
|
`${newRule.lookBack.interval}${newRule.lookBack.type}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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(() => {
|
beforeEach(() => {
|
||||||
esArchiverLoad('custom_rules');
|
esArchiverLoad('custom_rules');
|
||||||
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
|
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
|
||||||
|
@ -208,6 +226,7 @@ describe('Deletes custom rules', () => {
|
||||||
esArchiverUnload('custom_rules');
|
esArchiverUnload('custom_rules');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('Deletion', () => {
|
||||||
it('Deletes one rule', () => {
|
it('Deletes one rule', () => {
|
||||||
cy.get(RULES_TABLE)
|
cy.get(RULES_TABLE)
|
||||||
.find(RULES_ROW)
|
.find(RULES_ROW)
|
||||||
|
@ -215,22 +234,25 @@ describe('Deletes custom rules', () => {
|
||||||
const initialNumberOfRules = rules.length;
|
const initialNumberOfRules = rules.length;
|
||||||
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1;
|
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1;
|
||||||
|
|
||||||
cy.get(SHOWING_RULES_TEXT)
|
cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${initialNumberOfRules} rules`);
|
||||||
.invoke('text')
|
|
||||||
.should('eql', `Showing ${initialNumberOfRules} rules`);
|
|
||||||
|
|
||||||
deleteFirstRule();
|
deleteFirstRule();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
|
cy.wrap($table.find(RULES_ROW).length).should(
|
||||||
|
'eql',
|
||||||
|
expectedNumberOfRulesAfterDeletion
|
||||||
|
);
|
||||||
});
|
});
|
||||||
cy.get(SHOWING_RULES_TEXT)
|
cy.get(SHOWING_RULES_TEXT).should(
|
||||||
.invoke('text')
|
'have.text',
|
||||||
.should('eql', `Showing ${expectedNumberOfRulesAfterDeletion} rules`);
|
`Showing ${expectedNumberOfRulesAfterDeletion} rules`
|
||||||
cy.get(CUSTOM_RULES_BTN)
|
);
|
||||||
.invoke('text')
|
cy.get(CUSTOM_RULES_BTN).should(
|
||||||
.should('eql', `Custom rules (${expectedNumberOfRulesAfterDeletion})`);
|
'have.text',
|
||||||
|
`Custom rules (${expectedNumberOfRulesAfterDeletion})`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -240,47 +262,55 @@ describe('Deletes custom rules', () => {
|
||||||
.then((rules) => {
|
.then((rules) => {
|
||||||
const initialNumberOfRules = rules.length;
|
const initialNumberOfRules = rules.length;
|
||||||
const numberOfRulesToBeDeleted = 3;
|
const numberOfRulesToBeDeleted = 3;
|
||||||
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - numberOfRulesToBeDeleted;
|
const expectedNumberOfRulesAfterDeletion =
|
||||||
|
initialNumberOfRules - numberOfRulesToBeDeleted;
|
||||||
|
|
||||||
selectNumberOfRules(numberOfRulesToBeDeleted);
|
selectNumberOfRules(numberOfRulesToBeDeleted);
|
||||||
deleteSelectedRules();
|
deleteSelectedRules();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
|
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})`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
cy.get(SHOWING_RULES_TEXT)
|
|
||||||
.invoke('text')
|
|
||||||
.should('eql', `Showing ${expectedNumberOfRulesAfterDeletion} rule`);
|
|
||||||
cy.get(CUSTOM_RULES_BTN)
|
|
||||||
.invoke('text')
|
|
||||||
.should('eql', `Custom rules (${expectedNumberOfRulesAfterDeletion})`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('Edition', () => {
|
||||||
it('Allows a rule to be edited', () => {
|
it('Allows a rule to be edited', () => {
|
||||||
editFirstRule();
|
editFirstRule();
|
||||||
|
|
||||||
// expect define step to populate
|
// expect define step to populate
|
||||||
cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', existingRule.customQuery);
|
cy.get(CUSTOM_QUERY_INPUT).should('have.text', existingRule.customQuery);
|
||||||
if (existingRule.index && existingRule.index.length > 0) {
|
if (existingRule.index && existingRule.index.length > 0) {
|
||||||
cy.get(DEFINE_INDEX_INPUT).invoke('text').should('eq', existingRule.index.join(''));
|
cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.index.join(''));
|
||||||
}
|
}
|
||||||
|
|
||||||
goToAboutStepTab();
|
goToAboutStepTab();
|
||||||
|
|
||||||
// expect about step to populate
|
// expect about step to populate
|
||||||
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name);
|
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name);
|
||||||
cy.get(RULE_DESCRIPTION_INPUT).invoke('text').should('eql', existingRule.description);
|
cy.get(RULE_DESCRIPTION_INPUT).should('have.text', existingRule.description);
|
||||||
cy.get(TAGS_FIELD).invoke('text').should('eql', existingRule.tags.join(''));
|
cy.get(TAGS_FIELD).should('have.text', existingRule.tags.join(''));
|
||||||
|
cy.get(SEVERITY_DROPDOWN).should('have.text', existingRule.severity);
|
||||||
cy.get(SEVERITY_DROPDOWN).invoke('text').should('eql', existingRule.severity);
|
|
||||||
cy.get(RISK_INPUT).invoke('val').should('eql', existingRule.riskScore);
|
cy.get(RISK_INPUT).invoke('val').should('eql', existingRule.riskScore);
|
||||||
|
|
||||||
goToScheduleStepTab();
|
goToScheduleStepTab();
|
||||||
|
|
||||||
// expect schedule step to populate
|
// expect schedule step to populate
|
||||||
const intervalParts = existingRule.interval && existingRule.interval.match(/[0-9]+|[a-zA-Z]+/g);
|
const intervalParts =
|
||||||
|
existingRule.interval && existingRule.interval.match(/[0-9]+|[a-zA-Z]+/g);
|
||||||
if (intervalParts) {
|
if (intervalParts) {
|
||||||
const [amount, unit] = intervalParts;
|
const [amount, unit] = intervalParts;
|
||||||
cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount);
|
cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount);
|
||||||
|
@ -294,57 +324,34 @@ describe('Deletes custom rules', () => {
|
||||||
cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions');
|
cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions');
|
||||||
|
|
||||||
goToAboutStepTab();
|
goToAboutStepTab();
|
||||||
|
|
||||||
const editedRule = {
|
|
||||||
...existingRule,
|
|
||||||
severity: 'Medium',
|
|
||||||
description: 'Edited Rule description',
|
|
||||||
};
|
|
||||||
|
|
||||||
fillAboutRule(editedRule);
|
fillAboutRule(editedRule);
|
||||||
saveEditedRule();
|
saveEditedRule();
|
||||||
|
|
||||||
const expectedTags = editedRule.tags.join('');
|
cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name} Beta`);
|
||||||
const expectedIndexPatterns =
|
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description);
|
||||||
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(() => {
|
cy.get(ABOUT_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Severity').invoke('text').should('eql', editedRule.severity);
|
getDetails(SEVERITY_DETAILS).should('have.text', editedRule.severity);
|
||||||
getDescriptionForTitle('Risk score').invoke('text').should('eql', editedRule.riskScore);
|
getDetails(RISK_SCORE_DETAILS).should('have.text', editedRule.riskScore);
|
||||||
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
|
getDetails(TAGS_DETAILS).should('have.text', expectedEditedtags);
|
||||||
});
|
});
|
||||||
|
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE)
|
||||||
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
|
.eq(INVESTIGATION_NOTES_TOGGLE)
|
||||||
cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', editedRule.note);
|
.click({ force: true });
|
||||||
|
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', editedRule.note);
|
||||||
cy.get(DEFINITION_DETAILS).within(() => {
|
cy.get(DEFINITION_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Index patterns')
|
getDetails(INDEX_PATTERNS_DETAILS).should(
|
||||||
.invoke('text')
|
'have.text',
|
||||||
.should('eql', expectedIndexPatterns.join(''));
|
expectedEditedIndexPatterns.join('')
|
||||||
getDescriptionForTitle('Custom query')
|
);
|
||||||
.invoke('text')
|
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${editedRule.customQuery} `);
|
||||||
.should('eql', `${editedRule.customQuery} `);
|
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
|
||||||
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query');
|
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||||
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (editedRule.interval) {
|
if (editedRule.interval) {
|
||||||
cy.get(SCHEDULE_DETAILS).within(() => {
|
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Runs every').invoke('text').should('eql', editedRule.interval);
|
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.
|
* 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 {
|
import {
|
||||||
CUSTOM_RULES_BTN,
|
CUSTOM_RULES_BTN,
|
||||||
|
@ -12,19 +12,32 @@ import {
|
||||||
RULE_NAME,
|
RULE_NAME,
|
||||||
RULES_ROW,
|
RULES_ROW,
|
||||||
RULES_TABLE,
|
RULES_TABLE,
|
||||||
|
RULE_SWITCH,
|
||||||
SEVERITY,
|
SEVERITY,
|
||||||
} from '../screens/alerts_detection_rules';
|
} from '../screens/alerts_detection_rules';
|
||||||
import {
|
import {
|
||||||
ABOUT_DETAILS,
|
ABOUT_DETAILS,
|
||||||
ABOUT_INVESTIGATION_NOTES,
|
ABOUT_INVESTIGATION_NOTES,
|
||||||
ABOUT_RULE_DESCRIPTION,
|
ABOUT_RULE_DESCRIPTION,
|
||||||
|
ADDITIONAL_LOOK_BACK_DETAILS,
|
||||||
|
CUSTOM_QUERY_DETAILS,
|
||||||
DEFINITION_DETAILS,
|
DEFINITION_DETAILS,
|
||||||
getDescriptionForTitle,
|
FALSE_POSITIVES_DETAILS,
|
||||||
|
getDetails,
|
||||||
|
INDEX_PATTERNS_DETAILS,
|
||||||
INVESTIGATION_NOTES_MARKDOWN,
|
INVESTIGATION_NOTES_MARKDOWN,
|
||||||
INVESTIGATION_NOTES_TOGGLE,
|
INVESTIGATION_NOTES_TOGGLE,
|
||||||
|
MITRE_ATTACK_DETAILS,
|
||||||
|
REFERENCE_URLS_DETAILS,
|
||||||
|
RISK_SCORE_DETAILS,
|
||||||
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
|
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
|
||||||
RULE_NAME_HEADER,
|
RULE_NAME_HEADER,
|
||||||
|
RULE_TYPE_DETAILS,
|
||||||
|
RUNS_EVERY_DETAILS,
|
||||||
SCHEDULE_DETAILS,
|
SCHEDULE_DETAILS,
|
||||||
|
SEVERITY_DETAILS,
|
||||||
|
TAGS_DETAILS,
|
||||||
|
TIMELINE_TEMPLATE_DETAILS,
|
||||||
} from '../screens/rule_details';
|
} from '../screens/rule_details';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -43,14 +56,25 @@ import {
|
||||||
import {
|
import {
|
||||||
createAndActivateRule,
|
createAndActivateRule,
|
||||||
fillAboutRuleAndContinue,
|
fillAboutRuleAndContinue,
|
||||||
selectEqlRuleType,
|
|
||||||
fillDefineEqlRuleAndContinue,
|
fillDefineEqlRuleAndContinue,
|
||||||
|
fillScheduleRuleAndContinue,
|
||||||
|
selectEqlRuleType,
|
||||||
} from '../tasks/create_new_rule';
|
} from '../tasks/create_new_rule';
|
||||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||||
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
||||||
|
|
||||||
import { DETECTIONS_URL } from '../urls/navigation';
|
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', () => {
|
describe('Detection rules, EQL', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
esArchiverLoad('timeline');
|
esArchiverLoad('timeline');
|
||||||
|
@ -70,14 +94,14 @@ describe('Detection rules, EQL', () => {
|
||||||
selectEqlRuleType();
|
selectEqlRuleType();
|
||||||
fillDefineEqlRuleAndContinue(eqlRule);
|
fillDefineEqlRuleAndContinue(eqlRule);
|
||||||
fillAboutRuleAndContinue(eqlRule);
|
fillAboutRuleAndContinue(eqlRule);
|
||||||
|
fillScheduleRuleAndContinue(eqlRule);
|
||||||
createAndActivateRule();
|
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();
|
changeToThreeHundredRowsPerPage();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
|
||||||
const expectedNumberOfRules = 1;
|
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
|
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.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
||||||
});
|
});
|
||||||
cy.get(RULE_NAME).invoke('text').should('eql', eqlRule.name);
|
cy.get(RULE_NAME).should('have.text', eqlRule.name);
|
||||||
cy.get(RISK_SCORE).invoke('text').should('eql', eqlRule.riskScore);
|
cy.get(RISK_SCORE).should('have.text', eqlRule.riskScore);
|
||||||
cy.get(SEVERITY).invoke('text').should('eql', eqlRule.severity);
|
cy.get(SEVERITY).should('have.text', eqlRule.severity);
|
||||||
cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true');
|
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||||
|
|
||||||
goToRuleDetails();
|
goToRuleDetails();
|
||||||
|
|
||||||
let expectedUrls = '';
|
cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name} Beta`);
|
||||||
eqlRule.referenceUrls.forEach((url) => {
|
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', eqlRule.description);
|
||||||
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(ABOUT_DETAILS).within(() => {
|
cy.get(ABOUT_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Severity').invoke('text').should('eql', eqlRule.severity);
|
getDetails(SEVERITY_DETAILS).should('have.text', eqlRule.severity);
|
||||||
getDescriptionForTitle('Risk score').invoke('text').should('eql', eqlRule.riskScore);
|
getDetails(RISK_SCORE_DETAILS).should('have.text', eqlRule.riskScore);
|
||||||
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
|
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
|
||||||
getDescriptionForTitle('False positive examples')
|
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
|
||||||
.invoke('text')
|
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
|
||||||
.should('eql', expectedFalsePositives);
|
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
|
||||||
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
|
|
||||||
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
|
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(() => {
|
cy.get(DEFINITION_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Index patterns')
|
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
|
||||||
.invoke('text')
|
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${eqlRule.customQuery} `);
|
||||||
.should('eql', expectedIndexPatterns.join(''));
|
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation');
|
||||||
getDescriptionForTitle('Custom query')
|
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||||
.invoke('text')
|
|
||||||
.should('eql', `${eqlRule.customQuery} `);
|
|
||||||
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Event Correlation');
|
|
||||||
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(SCHEDULE_DETAILS).within(() => {
|
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
|
getDetails(RUNS_EVERY_DETAILS).should(
|
||||||
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
|
'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,
|
SEVERITY,
|
||||||
} from '../screens/alerts_detection_rules';
|
} from '../screens/alerts_detection_rules';
|
||||||
import {
|
import {
|
||||||
|
ABOUT_DETAILS,
|
||||||
ABOUT_RULE_DESCRIPTION,
|
ABOUT_RULE_DESCRIPTION,
|
||||||
|
ADDITIONAL_LOOK_BACK_DETAILS,
|
||||||
|
ANOMALY_SCORE_DETAILS,
|
||||||
|
DEFINITION_DETAILS,
|
||||||
|
FALSE_POSITIVES_DETAILS,
|
||||||
|
getDetails,
|
||||||
MACHINE_LEARNING_JOB_ID,
|
MACHINE_LEARNING_JOB_ID,
|
||||||
MACHINE_LEARNING_JOB_STATUS,
|
MACHINE_LEARNING_JOB_STATUS,
|
||||||
|
MITRE_ATTACK_DETAILS,
|
||||||
|
REFERENCE_URLS_DETAILS,
|
||||||
|
RISK_SCORE_DETAILS,
|
||||||
RULE_NAME_HEADER,
|
RULE_NAME_HEADER,
|
||||||
getDescriptionForTitle,
|
RULE_TYPE_DETAILS,
|
||||||
ABOUT_DETAILS,
|
RUNS_EVERY_DETAILS,
|
||||||
DEFINITION_DETAILS,
|
|
||||||
SCHEDULE_DETAILS,
|
SCHEDULE_DETAILS,
|
||||||
|
SEVERITY_DETAILS,
|
||||||
|
TAGS_DETAILS,
|
||||||
|
TIMELINE_TEMPLATE_DETAILS,
|
||||||
} from '../screens/rule_details';
|
} from '../screens/rule_details';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -43,6 +54,7 @@ import {
|
||||||
createAndActivateRule,
|
createAndActivateRule,
|
||||||
fillAboutRuleAndContinue,
|
fillAboutRuleAndContinue,
|
||||||
fillDefineMachineLearningRuleAndContinue,
|
fillDefineMachineLearningRuleAndContinue,
|
||||||
|
fillScheduleRuleAndContinue,
|
||||||
selectMachineLearningRuleType,
|
selectMachineLearningRuleType,
|
||||||
} from '../tasks/create_new_rule';
|
} from '../tasks/create_new_rule';
|
||||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||||
|
@ -50,6 +62,16 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
||||||
|
|
||||||
import { DETECTIONS_URL } from '../urls/navigation';
|
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', () => {
|
describe('Detection rules, machine learning', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
esArchiverLoad('prebuilt_rules_loaded');
|
esArchiverLoad('prebuilt_rules_loaded');
|
||||||
|
@ -69,6 +91,7 @@ describe('Detection rules, machine learning', () => {
|
||||||
selectMachineLearningRuleType();
|
selectMachineLearningRuleType();
|
||||||
fillDefineMachineLearningRuleAndContinue(machineLearningRule);
|
fillDefineMachineLearningRuleAndContinue(machineLearningRule);
|
||||||
fillAboutRuleAndContinue(machineLearningRule);
|
fillAboutRuleAndContinue(machineLearningRule);
|
||||||
|
fillScheduleRuleAndContinue(machineLearningRule);
|
||||||
createAndActivateRule();
|
createAndActivateRule();
|
||||||
|
|
||||||
cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');
|
cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)');
|
||||||
|
@ -76,7 +99,6 @@ describe('Detection rules, machine learning', () => {
|
||||||
changeToThreeHundredRowsPerPage();
|
changeToThreeHundredRowsPerPage();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
|
||||||
const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1;
|
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
|
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.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
||||||
});
|
});
|
||||||
cy.get(RULE_NAME).invoke('text').should('eql', machineLearningRule.name);
|
cy.get(RULE_NAME).should('have.text', machineLearningRule.name);
|
||||||
cy.get(RISK_SCORE).invoke('text').should('eql', machineLearningRule.riskScore);
|
cy.get(RISK_SCORE).should('have.text', machineLearningRule.riskScore);
|
||||||
cy.get(SEVERITY).invoke('text').should('eql', machineLearningRule.severity);
|
cy.get(SEVERITY).should('have.text', machineLearningRule.severity);
|
||||||
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||||
|
|
||||||
goToRuleDetails();
|
goToRuleDetails();
|
||||||
|
|
||||||
let expectedUrls = '';
|
cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name} Beta`);
|
||||||
machineLearningRule.referenceUrls.forEach((url) => {
|
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', machineLearningRule.description);
|
||||||
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(ABOUT_DETAILS).within(() => {
|
cy.get(ABOUT_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Severity').invoke('text').should('eql', machineLearningRule.severity);
|
getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity);
|
||||||
getDescriptionForTitle('Risk score')
|
getDetails(RISK_SCORE_DETAILS).should('have.text', machineLearningRule.riskScore);
|
||||||
.invoke('text')
|
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
|
||||||
.should('eql', machineLearningRule.riskScore);
|
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
|
||||||
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
|
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
|
||||||
getDescriptionForTitle('False positive examples')
|
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
|
||||||
.invoke('text')
|
|
||||||
.should('eql', expectedFalsePositives);
|
|
||||||
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
|
|
||||||
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(DEFINITION_DETAILS).within(() => {
|
cy.get(DEFINITION_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Anomaly score')
|
getDetails(ANOMALY_SCORE_DETAILS).should(
|
||||||
.invoke('text')
|
'have.text',
|
||||||
.should('eql', machineLearningRule.anomalyScoreThreshold);
|
machineLearningRule.anomalyScoreThreshold
|
||||||
getDescriptionForTitle('Anomaly score')
|
);
|
||||||
.invoke('text')
|
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Machine Learning');
|
||||||
.should('eql', machineLearningRule.anomalyScoreThreshold);
|
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||||
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Machine Learning');
|
cy.get(MACHINE_LEARNING_JOB_STATUS).should('have.text', 'Stopped');
|
||||||
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
|
cy.get(MACHINE_LEARNING_JOB_ID).should('have.text', machineLearningRule.machineLearningJob);
|
||||||
cy.get(MACHINE_LEARNING_JOB_STATUS).invoke('text').should('eql', 'Stopped');
|
|
||||||
cy.get(MACHINE_LEARNING_JOB_ID)
|
|
||||||
.invoke('text')
|
|
||||||
.should('eql', machineLearningRule.machineLearningJob);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(SCHEDULE_DETAILS).within(() => {
|
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
|
getDetails(RUNS_EVERY_DETAILS).should(
|
||||||
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
|
'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.
|
* 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 {
|
import {
|
||||||
CUSTOM_RULES_BTN,
|
CUSTOM_RULES_BTN,
|
||||||
RISK_SCORE,
|
RISK_SCORE,
|
||||||
RULE_NAME,
|
RULE_NAME,
|
||||||
|
RULE_SWITCH,
|
||||||
RULES_ROW,
|
RULES_ROW,
|
||||||
RULES_TABLE,
|
RULES_TABLE,
|
||||||
SEVERITY,
|
SEVERITY,
|
||||||
} from '../screens/alerts_detection_rules';
|
} from '../screens/alerts_detection_rules';
|
||||||
import {
|
import {
|
||||||
ABOUT_INVESTIGATION_NOTES,
|
ABOUT_INVESTIGATION_NOTES,
|
||||||
|
ABOUT_DETAILS,
|
||||||
ABOUT_RULE_DESCRIPTION,
|
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_MARKDOWN,
|
||||||
INVESTIGATION_NOTES_TOGGLE,
|
INVESTIGATION_NOTES_TOGGLE,
|
||||||
|
MITRE_ATTACK_DETAILS,
|
||||||
|
REFERENCE_URLS_DETAILS,
|
||||||
|
RISK_SCORE_DETAILS,
|
||||||
|
RISK_SCORE_OVERRIDE_DETAILS,
|
||||||
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
|
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
|
||||||
RULE_NAME_HEADER,
|
RULE_NAME_HEADER,
|
||||||
ABOUT_DETAILS,
|
RULE_NAME_OVERRIDE_DETAILS,
|
||||||
getDescriptionForTitle,
|
RULE_TYPE_DETAILS,
|
||||||
DEFINITION_DETAILS,
|
RUNS_EVERY_DETAILS,
|
||||||
SCHEDULE_DETAILS,
|
SCHEDULE_DETAILS,
|
||||||
DETAILS_TITLE,
|
SEVERITY_DETAILS,
|
||||||
DETAILS_DESCRIPTION,
|
TAGS_DETAILS,
|
||||||
|
TIMELINE_TEMPLATE_DETAILS,
|
||||||
|
TIMESTAMP_OVERRIDE_DETAILS,
|
||||||
} from '../screens/rule_details';
|
} from '../screens/rule_details';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
goToManageAlertsDetectionRules,
|
goToManageAlertsDetectionRules,
|
||||||
|
sortRiskScore,
|
||||||
waitForAlertsIndexToBeCreated,
|
waitForAlertsIndexToBeCreated,
|
||||||
waitForAlertsPanelToBeLoaded,
|
waitForAlertsPanelToBeLoaded,
|
||||||
} from '../tasks/alerts';
|
} from '../tasks/alerts';
|
||||||
|
@ -46,12 +71,24 @@ import {
|
||||||
createAndActivateRule,
|
createAndActivateRule,
|
||||||
fillAboutRuleWithOverrideAndContinue,
|
fillAboutRuleWithOverrideAndContinue,
|
||||||
fillDefineCustomRuleWithImportedQueryAndContinue,
|
fillDefineCustomRuleWithImportedQueryAndContinue,
|
||||||
|
fillScheduleRuleAndContinue,
|
||||||
|
waitForTheRuleToBeExecuted,
|
||||||
} from '../tasks/create_new_rule';
|
} from '../tasks/create_new_rule';
|
||||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||||
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
||||||
|
import { refreshPage } from '../tasks/security_header';
|
||||||
|
|
||||||
import { DETECTIONS_URL } from '../urls/navigation';
|
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', () => {
|
describe('Detection rules, override', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
esArchiverLoad('timeline');
|
esArchiverLoad('timeline');
|
||||||
|
@ -70,9 +107,10 @@ describe('Detection rules, override', () => {
|
||||||
goToCreateNewRule();
|
goToCreateNewRule();
|
||||||
fillDefineCustomRuleWithImportedQueryAndContinue(newOverrideRule);
|
fillDefineCustomRuleWithImportedQueryAndContinue(newOverrideRule);
|
||||||
fillAboutRuleWithOverrideAndContinue(newOverrideRule);
|
fillAboutRuleWithOverrideAndContinue(newOverrideRule);
|
||||||
|
fillScheduleRuleAndContinue(newOverrideRule);
|
||||||
createAndActivateRule();
|
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();
|
changeToThreeHundredRowsPerPage();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
@ -87,98 +125,75 @@ describe('Detection rules, override', () => {
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
||||||
});
|
});
|
||||||
cy.get(RULE_NAME).invoke('text').should('eql', newOverrideRule.name);
|
cy.get(RULE_NAME).should('have.text', newOverrideRule.name);
|
||||||
cy.get(RISK_SCORE).invoke('text').should('eql', newOverrideRule.riskScore);
|
cy.get(RISK_SCORE).should('have.text', newOverrideRule.riskScore);
|
||||||
cy.get(SEVERITY).invoke('text').should('eql', newOverrideRule.severity);
|
cy.get(SEVERITY).should('have.text', newOverrideRule.severity);
|
||||||
cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true');
|
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||||
|
|
||||||
goToRuleDetails();
|
goToRuleDetails();
|
||||||
|
|
||||||
let expectedUrls = '';
|
cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name} Beta`);
|
||||||
newOverrideRule.referenceUrls.forEach((url) => {
|
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newOverrideRule.description);
|
||||||
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(ABOUT_DETAILS).within(() => {
|
cy.get(ABOUT_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Severity').invoke('text').should('eql', newOverrideRule.severity);
|
getDetails(SEVERITY_DETAILS).should('have.text', newOverrideRule.severity);
|
||||||
getDescriptionForTitle('Risk score').invoke('text').should('eql', newOverrideRule.riskScore);
|
getDetails(RISK_SCORE_DETAILS).should('have.text', newOverrideRule.riskScore);
|
||||||
getDescriptionForTitle('Risk score override')
|
getDetails(RISK_SCORE_OVERRIDE_DETAILS).should(
|
||||||
.invoke('text')
|
'have.text',
|
||||||
.should('eql', `${newOverrideRule.riskOverride}signal.rule.risk_score`);
|
`${newOverrideRule.riskOverride}signal.rule.risk_score`
|
||||||
getDescriptionForTitle('Rule name override')
|
);
|
||||||
.invoke('text')
|
getDetails(RULE_NAME_OVERRIDE_DETAILS).should('have.text', newOverrideRule.nameOverride);
|
||||||
.should('eql', newOverrideRule.nameOverride);
|
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
|
||||||
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
|
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
|
||||||
getDescriptionForTitle('False positive examples')
|
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
|
||||||
.invoke('text')
|
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
|
||||||
.should('eql', expectedFalsePositives);
|
getDetails(TIMESTAMP_OVERRIDE_DETAILS).should('have.text', newOverrideRule.timestampOverride);
|
||||||
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);
|
|
||||||
cy.contains(DETAILS_TITLE, 'Severity override')
|
cy.contains(DETAILS_TITLE, 'Severity override')
|
||||||
.invoke('index', DETAILS_TITLE) // get index relative to other titles, not all siblings
|
.invoke('index', DETAILS_TITLE) // get index relative to other titles, not all siblings
|
||||||
.then((severityOverrideIndex) => {
|
.then((severityOverrideIndex) => {
|
||||||
newOverrideRule.severityOverride.forEach((severity, i) => {
|
newOverrideRule.severityOverride.forEach((severity, i) => {
|
||||||
cy.get(DETAILS_DESCRIPTION)
|
cy.get(DETAILS_DESCRIPTION)
|
||||||
.eq(severityOverrideIndex + i)
|
.eq(severityOverrideIndex + i)
|
||||||
.invoke('text')
|
|
||||||
.should(
|
.should(
|
||||||
'eql',
|
'have.text',
|
||||||
`${severity.sourceField}:${severity.sourceValue}${expectedOverrideSeverities[i]}`
|
`${severity.sourceField}:${severity.sourceValue}${severitiesOverride[i]}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
|
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(() => {
|
cy.get(DEFINITION_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Index patterns')
|
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
|
||||||
.invoke('text')
|
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newOverrideRule.customQuery} `);
|
||||||
.should('eql', expectedIndexPatterns.join(''));
|
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
|
||||||
getDescriptionForTitle('Custom query')
|
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||||
.invoke('text')
|
});
|
||||||
.should('eql', `${newOverrideRule.customQuery} `);
|
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query');
|
getDetails(RUNS_EVERY_DETAILS).should(
|
||||||
getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None');
|
'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(() => {
|
refreshPage();
|
||||||
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
|
waitForTheRuleToBeExecuted();
|
||||||
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
|
|
||||||
});
|
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();
|
loadPrebuiltDetectionRules();
|
||||||
waitForPrebuiltDetectionRulesToBeLoaded();
|
waitForPrebuiltDetectionRulesToBeLoaded();
|
||||||
|
|
||||||
cy.get(ELASTIC_RULES_BTN).invoke('text').should('eql', expectedElasticRulesBtnText);
|
cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText);
|
||||||
|
|
||||||
changeToThreeHundredRowsPerPage();
|
changeToThreeHundredRowsPerPage();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
@ -81,7 +81,7 @@ describe('Deleting prebuilt rules', () => {
|
||||||
loadPrebuiltDetectionRules();
|
loadPrebuiltDetectionRules();
|
||||||
waitForPrebuiltDetectionRulesToBeLoaded();
|
waitForPrebuiltDetectionRulesToBeLoaded();
|
||||||
|
|
||||||
cy.get(ELASTIC_RULES_BTN).invoke('text').should('eql', expectedElasticRulesBtnText);
|
cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText);
|
||||||
|
|
||||||
changeToThreeHundredRowsPerPage();
|
changeToThreeHundredRowsPerPage();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
@ -113,16 +113,15 @@ describe('Deleting prebuilt rules', () => {
|
||||||
changeToThreeHundredRowsPerPage();
|
changeToThreeHundredRowsPerPage();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
|
||||||
cy.get(ELASTIC_RULES_BTN)
|
cy.get(ELASTIC_RULES_BTN).should(
|
||||||
.invoke('text')
|
'have.text',
|
||||||
.should('eql', `Elastic rules (${expectedNumberOfRulesAfterDeletion})`);
|
`Elastic rules (${expectedNumberOfRulesAfterDeletion})`
|
||||||
|
);
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
|
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
|
||||||
});
|
});
|
||||||
cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist');
|
cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist');
|
||||||
cy.get(RELOAD_PREBUILT_RULES_BTN)
|
cy.get(RELOAD_PREBUILT_RULES_BTN).should('have.text', 'Install 1 Elastic prebuilt rule ');
|
||||||
.invoke('text')
|
|
||||||
.should('eql', 'Install 1 Elastic prebuilt rule ');
|
|
||||||
|
|
||||||
reloadDeletedRules();
|
reloadDeletedRules();
|
||||||
|
|
||||||
|
@ -135,9 +134,10 @@ describe('Deleting prebuilt rules', () => {
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering);
|
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering);
|
||||||
});
|
});
|
||||||
cy.get(ELASTIC_RULES_BTN)
|
cy.get(ELASTIC_RULES_BTN).should(
|
||||||
.invoke('text')
|
'have.text',
|
||||||
.should('eql', `Elastic rules (${expectedNumberOfRulesAfterRecovering})`);
|
`Elastic rules (${expectedNumberOfRulesAfterRecovering})`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Deletes and recovers more than one rule', () => {
|
it('Deletes and recovers more than one rule', () => {
|
||||||
|
@ -152,12 +152,14 @@ describe('Deleting prebuilt rules', () => {
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
|
||||||
cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist');
|
cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist');
|
||||||
cy.get(RELOAD_PREBUILT_RULES_BTN)
|
cy.get(RELOAD_PREBUILT_RULES_BTN).should(
|
||||||
.invoke('text')
|
'have.text',
|
||||||
.should('eql', `Install ${numberOfRulesToBeSelected} Elastic prebuilt rules `);
|
`Install ${numberOfRulesToBeSelected} Elastic prebuilt rules `
|
||||||
cy.get(ELASTIC_RULES_BTN)
|
);
|
||||||
.invoke('text')
|
cy.get(ELASTIC_RULES_BTN).should(
|
||||||
.should('eql', `Elastic rules (${expectedNumberOfRulesAfterDeletion})`);
|
'have.text',
|
||||||
|
`Elastic rules (${expectedNumberOfRulesAfterDeletion})`
|
||||||
|
);
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterDeletion);
|
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.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering);
|
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRulesAfterRecovering);
|
||||||
});
|
});
|
||||||
cy.get(ELASTIC_RULES_BTN)
|
cy.get(ELASTIC_RULES_BTN).should(
|
||||||
.invoke('text')
|
'have.text',
|
||||||
.should('eql', `Elastic rules (${expectedNumberOfRulesAfterRecovering})`);
|
`Elastic rules (${expectedNumberOfRulesAfterRecovering})`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,27 +4,49 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 {
|
import {
|
||||||
CUSTOM_RULES_BTN,
|
CUSTOM_RULES_BTN,
|
||||||
RISK_SCORE,
|
RISK_SCORE,
|
||||||
RULE_NAME,
|
RULE_NAME,
|
||||||
|
RULE_SWITCH,
|
||||||
RULES_ROW,
|
RULES_ROW,
|
||||||
RULES_TABLE,
|
RULES_TABLE,
|
||||||
SEVERITY,
|
SEVERITY,
|
||||||
} from '../screens/alerts_detection_rules';
|
} from '../screens/alerts_detection_rules';
|
||||||
import {
|
import {
|
||||||
|
ABOUT_DETAILS,
|
||||||
ABOUT_INVESTIGATION_NOTES,
|
ABOUT_INVESTIGATION_NOTES,
|
||||||
ABOUT_RULE_DESCRIPTION,
|
ABOUT_RULE_DESCRIPTION,
|
||||||
|
ADDITIONAL_LOOK_BACK_DETAILS,
|
||||||
|
CUSTOM_QUERY_DETAILS,
|
||||||
|
FALSE_POSITIVES_DETAILS,
|
||||||
|
DEFINITION_DETAILS,
|
||||||
|
getDetails,
|
||||||
|
INDEX_PATTERNS_DETAILS,
|
||||||
INVESTIGATION_NOTES_MARKDOWN,
|
INVESTIGATION_NOTES_MARKDOWN,
|
||||||
INVESTIGATION_NOTES_TOGGLE,
|
INVESTIGATION_NOTES_TOGGLE,
|
||||||
|
MITRE_ATTACK_DETAILS,
|
||||||
|
REFERENCE_URLS_DETAILS,
|
||||||
|
RISK_SCORE_DETAILS,
|
||||||
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
|
RULE_ABOUT_DETAILS_HEADER_TOGGLE,
|
||||||
RULE_NAME_HEADER,
|
RULE_NAME_HEADER,
|
||||||
getDescriptionForTitle,
|
RULE_TYPE_DETAILS,
|
||||||
ABOUT_DETAILS,
|
RUNS_EVERY_DETAILS,
|
||||||
DEFINITION_DETAILS,
|
|
||||||
SCHEDULE_DETAILS,
|
SCHEDULE_DETAILS,
|
||||||
|
SEVERITY_DETAILS,
|
||||||
|
TAGS_DETAILS,
|
||||||
|
THRESHOLD_DETAILS,
|
||||||
|
TIMELINE_TEMPLATE_DETAILS,
|
||||||
} from '../screens/rule_details';
|
} from '../screens/rule_details';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -44,13 +66,25 @@ import {
|
||||||
createAndActivateRule,
|
createAndActivateRule,
|
||||||
fillAboutRuleAndContinue,
|
fillAboutRuleAndContinue,
|
||||||
fillDefineThresholdRuleAndContinue,
|
fillDefineThresholdRuleAndContinue,
|
||||||
|
fillScheduleRuleAndContinue,
|
||||||
selectThresholdRuleType,
|
selectThresholdRuleType,
|
||||||
|
waitForTheRuleToBeExecuted,
|
||||||
} from '../tasks/create_new_rule';
|
} from '../tasks/create_new_rule';
|
||||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||||
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
|
||||||
|
import { refreshPage } from '../tasks/security_header';
|
||||||
|
|
||||||
import { DETECTIONS_URL } from '../urls/navigation';
|
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', () => {
|
describe('Detection rules, threshold', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
esArchiverLoad('timeline');
|
esArchiverLoad('timeline');
|
||||||
|
@ -70,9 +104,10 @@ describe('Detection rules, threshold', () => {
|
||||||
selectThresholdRuleType();
|
selectThresholdRuleType();
|
||||||
fillDefineThresholdRuleAndContinue(newThresholdRule);
|
fillDefineThresholdRuleAndContinue(newThresholdRule);
|
||||||
fillAboutRuleAndContinue(newThresholdRule);
|
fillAboutRuleAndContinue(newThresholdRule);
|
||||||
|
fillScheduleRuleAndContinue(newThresholdRule);
|
||||||
createAndActivateRule();
|
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();
|
changeToThreeHundredRowsPerPage();
|
||||||
waitForRulesToBeLoaded();
|
waitForRulesToBeLoaded();
|
||||||
|
@ -87,79 +122,60 @@ describe('Detection rules, threshold', () => {
|
||||||
cy.get(RULES_TABLE).then(($table) => {
|
cy.get(RULES_TABLE).then(($table) => {
|
||||||
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
|
||||||
});
|
});
|
||||||
cy.get(RULE_NAME).invoke('text').should('eql', newThresholdRule.name);
|
cy.get(RULE_NAME).should('have.text', newThresholdRule.name);
|
||||||
cy.get(RISK_SCORE).invoke('text').should('eql', newThresholdRule.riskScore);
|
cy.get(RISK_SCORE).should('have.text', newThresholdRule.riskScore);
|
||||||
cy.get(SEVERITY).invoke('text').should('eql', newThresholdRule.severity);
|
cy.get(SEVERITY).should('have.text', newThresholdRule.severity);
|
||||||
cy.get('[data-test-subj="rule-switch"]').should('have.attr', 'aria-checked', 'true');
|
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
|
||||||
|
|
||||||
goToRuleDetails();
|
goToRuleDetails();
|
||||||
|
|
||||||
let expectedUrls = '';
|
cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name} Beta`);
|
||||||
newThresholdRule.referenceUrls.forEach((url) => {
|
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThresholdRule.description);
|
||||||
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(ABOUT_DETAILS).within(() => {
|
cy.get(ABOUT_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Severity').invoke('text').should('eql', newThresholdRule.severity);
|
getDetails(SEVERITY_DETAILS).should('have.text', newThresholdRule.severity);
|
||||||
getDescriptionForTitle('Risk score').invoke('text').should('eql', newThresholdRule.riskScore);
|
getDetails(RISK_SCORE_DETAILS).should('have.text', newThresholdRule.riskScore);
|
||||||
getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls);
|
getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls);
|
||||||
getDescriptionForTitle('False positive examples')
|
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
|
||||||
.invoke('text')
|
getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre);
|
||||||
.should('eql', expectedFalsePositives);
|
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
|
||||||
getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre);
|
|
||||||
getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
|
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(() => {
|
cy.get(DEFINITION_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Index patterns')
|
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
|
||||||
.invoke('text')
|
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newThresholdRule.customQuery} `);
|
||||||
.should('eql', expectedIndexPatterns.join(''));
|
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold');
|
||||||
getDescriptionForTitle('Custom query')
|
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
|
||||||
.invoke('text')
|
getDetails(THRESHOLD_DETAILS).should(
|
||||||
.should('eql', `${newThresholdRule.customQuery} `);
|
'have.text',
|
||||||
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}`
|
`Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(SCHEDULE_DETAILS).within(() => {
|
cy.get(SCHEDULE_DETAILS).within(() => {
|
||||||
getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m');
|
getDetails(RUNS_EVERY_DETAILS).should(
|
||||||
getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m');
|
'have.text',
|
||||||
});
|
`${newThresholdRule.runsEvery.interval}${newThresholdRule.runsEvery.type}`
|
||||||
|
);
|
||||||
|
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
|
||||||
|
'have.text',
|
||||||
|
`${newThresholdRule.lookBack.interval}${newThresholdRule.lookBack.type}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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')
|
.invoke('text')
|
||||||
.then((eventId) => {
|
.then((eventId) => {
|
||||||
investigateFirstAlertInTimeline();
|
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';
|
import { HOSTS_URL, NETWORK_URL } from '../urls/navigation';
|
||||||
|
|
||||||
// FLAKY: https://github.com/elastic/kibana/issues/78496
|
describe('Inspect', () => {
|
||||||
describe.skip('Inspect', () => {
|
|
||||||
context('Hosts stats and tables', () => {
|
context('Hosts stats and tables', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
loginAndWaitForPage(HOSTS_URL);
|
loginAndWaitForPage(HOSTS_URL);
|
||||||
|
|
|
@ -23,6 +23,12 @@ interface SeverityOverride {
|
||||||
sourceValue: string;
|
sourceValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Interval {
|
||||||
|
interval: string;
|
||||||
|
timeType: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomRule {
|
export interface CustomRule {
|
||||||
customQuery: string;
|
customQuery: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -38,6 +44,8 @@ export interface CustomRule {
|
||||||
mitre: Mitre[];
|
mitre: Mitre[];
|
||||||
note: string;
|
note: string;
|
||||||
timelineId: string;
|
timelineId: string;
|
||||||
|
runsEvery: Interval;
|
||||||
|
lookBack: Interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThresholdRule extends CustomRule {
|
export interface ThresholdRule extends CustomRule {
|
||||||
|
@ -65,6 +73,8 @@ export interface MachineLearningRule {
|
||||||
falsePositivesExamples: string[];
|
falsePositivesExamples: string[];
|
||||||
mitre: Mitre[];
|
mitre: Mitre[];
|
||||||
note: string;
|
note: string;
|
||||||
|
runsEvery: Interval;
|
||||||
|
lookBack: Interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mitre1: Mitre = {
|
const mitre1: Mitre = {
|
||||||
|
@ -83,8 +93,8 @@ const severityOverride1: SeverityOverride = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const severityOverride2: SeverityOverride = {
|
const severityOverride2: SeverityOverride = {
|
||||||
sourceField: 'agent.type',
|
sourceField: '@timestamp',
|
||||||
sourceValue: 'endpoint',
|
sourceValue: '10/02/2020',
|
||||||
};
|
};
|
||||||
|
|
||||||
const severityOverride3: SeverityOverride = {
|
const severityOverride3: SeverityOverride = {
|
||||||
|
@ -93,8 +103,20 @@ const severityOverride3: SeverityOverride = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const severityOverride4: SeverityOverride = {
|
const severityOverride4: SeverityOverride = {
|
||||||
sourceField: '@timestamp',
|
sourceField: 'agent.type',
|
||||||
sourceValue: '10/02/2020',
|
sourceValue: 'auditbeat',
|
||||||
|
};
|
||||||
|
|
||||||
|
const runsEvery: Interval = {
|
||||||
|
interval: '1',
|
||||||
|
timeType: 'Seconds',
|
||||||
|
type: 's',
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookBack: Interval = {
|
||||||
|
interval: '17520',
|
||||||
|
timeType: 'Hours',
|
||||||
|
type: 'h',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const newRule: CustomRule = {
|
export const newRule: CustomRule = {
|
||||||
|
@ -109,6 +131,8 @@ export const newRule: CustomRule = {
|
||||||
mitre: [mitre1, mitre2],
|
mitre: [mitre1, mitre2],
|
||||||
note: '# test markdown',
|
note: '# test markdown',
|
||||||
timelineId: '0162c130-78be-11ea-9718-118a926974a4',
|
timelineId: '0162c130-78be-11ea-9718-118a926974a4',
|
||||||
|
runsEvery,
|
||||||
|
lookBack,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const existingRule: CustomRule = {
|
export const existingRule: CustomRule = {
|
||||||
|
@ -132,6 +156,8 @@ export const existingRule: CustomRule = {
|
||||||
mitre: [],
|
mitre: [],
|
||||||
note: 'This is my note',
|
note: 'This is my note',
|
||||||
timelineId: '',
|
timelineId: '',
|
||||||
|
runsEvery,
|
||||||
|
lookBack,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const newOverrideRule: OverrideRule = {
|
export const newOverrideRule: OverrideRule = {
|
||||||
|
@ -150,6 +176,8 @@ export const newOverrideRule: OverrideRule = {
|
||||||
riskOverride: 'destination.port',
|
riskOverride: 'destination.port',
|
||||||
nameOverride: 'agent.type',
|
nameOverride: 'agent.type',
|
||||||
timestampOverride: '@timestamp',
|
timestampOverride: '@timestamp',
|
||||||
|
runsEvery,
|
||||||
|
lookBack,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const newThresholdRule: ThresholdRule = {
|
export const newThresholdRule: ThresholdRule = {
|
||||||
|
@ -166,6 +194,8 @@ export const newThresholdRule: ThresholdRule = {
|
||||||
timelineId: '0162c130-78be-11ea-9718-118a926974a4',
|
timelineId: '0162c130-78be-11ea-9718-118a926974a4',
|
||||||
thresholdField: 'host.name',
|
thresholdField: 'host.name',
|
||||||
threshold: '10',
|
threshold: '10',
|
||||||
|
runsEvery,
|
||||||
|
lookBack,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const machineLearningRule: MachineLearningRule = {
|
export const machineLearningRule: MachineLearningRule = {
|
||||||
|
@ -180,6 +210,8 @@ export const machineLearningRule: MachineLearningRule = {
|
||||||
falsePositivesExamples: ['False1'],
|
falsePositivesExamples: ['False1'],
|
||||||
mitre: [mitre1],
|
mitre: [mitre1],
|
||||||
note: '# test markdown',
|
note: '# test markdown',
|
||||||
|
runsEvery,
|
||||||
|
lookBack,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const eqlRule: CustomRule = {
|
export const eqlRule: CustomRule = {
|
||||||
|
@ -194,4 +226,24 @@ export const eqlRule: CustomRule = {
|
||||||
mitre: [mitre1, mitre2],
|
mitre: [mitre1, mitre2],
|
||||||
note: '# test markdown',
|
note: '# test markdown',
|
||||||
timelineId: '0162c130-78be-11ea-9718-118a926974a4',
|
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.
|
* 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 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 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 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 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 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 SELECTED_ALERTS = '[data-test-subj="selectedAlerts"]';
|
||||||
|
|
||||||
export const SEND_ALERT_TO_TIMELINE_BTN = '[data-test-subj="send-alert-to-timeline-button"]';
|
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 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 TAKE_ACTION_POPOVER_BTN = '[data-test-subj="alertActionPopover"] button';
|
||||||
|
|
||||||
export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-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 =
|
export const FALSE_POSITIVES_INPUT =
|
||||||
'[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] 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_DROPDOWN = '[data-test-subj="mlJobSelect"] button';
|
||||||
|
|
||||||
export const MACHINE_LEARNING_LIST = '.euiContextMenuItem__text';
|
export const MACHINE_LEARNING_LIST = '.euiContextMenuItem__text';
|
||||||
|
@ -73,6 +79,8 @@ export const MITRE_TECHNIQUES_INPUT =
|
||||||
export const REFERENCE_URLS_INPUT =
|
export const REFERENCE_URLS_INPUT =
|
||||||
'[data-test-subj="detectionEngineStepAboutRuleReferenceUrls"] input';
|
'[data-test-subj="detectionEngineStepAboutRuleReferenceUrls"] input';
|
||||||
|
|
||||||
|
export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]';
|
||||||
|
|
||||||
export const RISK_INPUT = '.euiRangeInput';
|
export const RISK_INPUT = '.euiRangeInput';
|
||||||
|
|
||||||
export const RISK_MAPPING_OVERRIDE_OPTION = '#risk_score-mapping-override';
|
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_NAME_OVERRIDE = '[data-test-subj="detectionEngineStepAboutRuleRuleNameOverride"]';
|
||||||
|
|
||||||
|
export const RULE_STATUS = '[data-test-subj="ruleStatus"]';
|
||||||
|
|
||||||
export const RULE_TIMESTAMP_OVERRIDE =
|
export const RULE_TIMESTAMP_OVERRIDE =
|
||||||
'[data-test-subj="detectionEngineStepAboutRuleTimestampOverride"]';
|
'[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_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]';
|
||||||
|
|
||||||
export const SCHEDULE_EDIT_TAB = '[data-test-subj="edit-rule-schedule-tab"]';
|
export const SCHEDULE_EDIT_TAB = '[data-test-subj="edit-rule-schedule-tab"]';
|
||||||
|
|
||||||
export const SCHEDULE_INTERVAL_AMOUNT_INPUT =
|
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 =
|
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 =
|
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 =
|
export const SCHEDULE_LOOKBACK_UNITS_INPUT =
|
||||||
'[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-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.
|
* 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_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]';
|
||||||
|
|
||||||
export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]';
|
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 =
|
export const ABOUT_DETAILS =
|
||||||
'[data-test-subj="aboutRule"] [data-test-subj="listItemColumnStepRuleDescription"]';
|
'[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 =
|
export const DEFINITION_DETAILS =
|
||||||
'[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"]';
|
'[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_MARKDOWN = 'test markdown';
|
||||||
|
|
||||||
export const INVESTIGATION_NOTES_TOGGLE = 1;
|
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 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_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]';
|
||||||
|
|
||||||
export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]';
|
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 =
|
export const SCHEDULE_DETAILS =
|
||||||
'[data-test-subj=schedule] [data-test-subj="listItemColumnStepRuleDescription"]';
|
'[data-test-subj=schedule] [data-test-subj="listItemColumnStepRuleDescription"]';
|
||||||
|
|
||||||
export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description';
|
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,
|
OPEN_SELECTED_ALERTS_BTN,
|
||||||
MARK_ALERT_IN_PROGRESS_BTN,
|
MARK_ALERT_IN_PROGRESS_BTN,
|
||||||
MARK_SELECTED_ALERTS_IN_PROGRESS_BTN,
|
MARK_SELECTED_ALERTS_IN_PROGRESS_BTN,
|
||||||
|
ALERT_RISK_SCORE_HEADER,
|
||||||
} from '../screens/alerts';
|
} from '../screens/alerts';
|
||||||
import { REFRESH_BUTTON } from '../screens/security_header';
|
import { REFRESH_BUTTON } from '../screens/security_header';
|
||||||
|
import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline';
|
||||||
|
|
||||||
export const closeFirstAlert = () => {
|
export const closeFirstAlert = () => {
|
||||||
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true });
|
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 = () => {
|
export const investigateFirstAlertInTimeline = () => {
|
||||||
cy.get(SEND_ALERT_TO_TIMELINE_BTN).first().click({ force: true });
|
cy.get(SEND_ALERT_TO_TIMELINE_BTN).first().click({ force: true });
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,6 +28,8 @@ import {
|
||||||
IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK,
|
IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK,
|
||||||
INPUT,
|
INPUT,
|
||||||
INVESTIGATION_NOTES_TEXTAREA,
|
INVESTIGATION_NOTES_TEXTAREA,
|
||||||
|
LOOK_BACK_INTERVAL,
|
||||||
|
LOOK_BACK_TIME_TYPE,
|
||||||
MACHINE_LEARNING_DROPDOWN,
|
MACHINE_LEARNING_DROPDOWN,
|
||||||
MACHINE_LEARNING_LIST,
|
MACHINE_LEARNING_LIST,
|
||||||
MACHINE_LEARNING_TYPE,
|
MACHINE_LEARNING_TYPE,
|
||||||
|
@ -36,13 +38,17 @@ import {
|
||||||
MITRE_TACTIC_DROPDOWN,
|
MITRE_TACTIC_DROPDOWN,
|
||||||
MITRE_TECHNIQUES_INPUT,
|
MITRE_TECHNIQUES_INPUT,
|
||||||
REFERENCE_URLS_INPUT,
|
REFERENCE_URLS_INPUT,
|
||||||
|
REFRESH_BUTTON,
|
||||||
RISK_INPUT,
|
RISK_INPUT,
|
||||||
RISK_MAPPING_OVERRIDE_OPTION,
|
RISK_MAPPING_OVERRIDE_OPTION,
|
||||||
RISK_OVERRIDE,
|
RISK_OVERRIDE,
|
||||||
RULE_DESCRIPTION_INPUT,
|
RULE_DESCRIPTION_INPUT,
|
||||||
RULE_NAME_INPUT,
|
RULE_NAME_INPUT,
|
||||||
RULE_NAME_OVERRIDE,
|
RULE_NAME_OVERRIDE,
|
||||||
|
RULE_STATUS,
|
||||||
RULE_TIMESTAMP_OVERRIDE,
|
RULE_TIMESTAMP_OVERRIDE,
|
||||||
|
RUNS_EVERY_INTERVAL,
|
||||||
|
RUNS_EVERY_TIME_TYPE,
|
||||||
SCHEDULE_CONTINUE_BUTTON,
|
SCHEDULE_CONTINUE_BUTTON,
|
||||||
SCHEDULE_EDIT_TAB,
|
SCHEDULE_EDIT_TAB,
|
||||||
SEVERITY_DROPDOWN,
|
SEVERITY_DROPDOWN,
|
||||||
|
@ -190,6 +196,13 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = (
|
||||||
cy.get(CUSTOM_QUERY_INPUT).should('not.exist');
|
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) => {
|
export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => {
|
||||||
const thresholdField = 0;
|
const thresholdField = 0;
|
||||||
const threshold = 1;
|
const threshold = 1;
|
||||||
|
@ -251,6 +264,14 @@ export const selectThresholdRuleType = () => {
|
||||||
cy.get(THRESHOLD_TYPE).click({ force: true });
|
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 = () => {
|
export const selectEqlRuleType = () => {
|
||||||
cy.get(EQL_TYPE).click({ force: true });
|
cy.get(EQL_TYPE).click({ force: true });
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,3 +21,7 @@ export const navigateFromHeaderTo = (page: string) => {
|
||||||
export const refreshPage = () => {
|
export const refreshPage = () => {
|
||||||
cy.get(REFRESH_BUTTON).click({ force: true }).invoke('text').should('not.equal', 'Updating');
|
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": "cypress open --config-file ./cypress/cypress.json",
|
||||||
"cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts",
|
"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": "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"
|
"test:generate": "node scripts/endpoint/resolver_generator"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -69,7 +69,9 @@ const RuleStatusComponent: React.FC<RuleStatusProps> = ({ ruleId, ruleEnabled })
|
||||||
<>
|
<>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiHealth color={getStatusColor(currentStatus?.status ?? null)}>
|
<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>
|
</EuiHealth>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{currentStatus?.status_date != null && currentStatus?.status != null && (
|
{currentStatus?.status_date != null && currentStatus?.status != null && (
|
||||||
|
@ -84,6 +86,7 @@ const RuleStatusComponent: React.FC<RuleStatusProps> = ({ ruleId, ruleEnabled })
|
||||||
)}
|
)}
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiButtonIcon
|
<EuiButtonIcon
|
||||||
|
data-test-subj="refreshButton"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
iconType="refresh"
|
iconType="refresh"
|
||||||
|
|
|
@ -145,21 +145,21 @@ export const ScheduleItem = ({
|
||||||
<EuiFormControlLayout
|
<EuiFormControlLayout
|
||||||
append={
|
append={
|
||||||
<MyEuiSelect
|
<MyEuiSelect
|
||||||
data-test-subj="schedule-units-input"
|
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
options={timeTypeOptions}
|
options={timeTypeOptions}
|
||||||
onChange={onChangeTimeType}
|
onChange={onChangeTimeType}
|
||||||
value={timeType}
|
value={timeType}
|
||||||
|
data-test-subj="timeType"
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<EuiFieldNumber
|
<EuiFieldNumber
|
||||||
data-test-subj="schedule-amount-input"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
min={minimumValue}
|
min={minimumValue}
|
||||||
onChange={onChangeTimeVal}
|
onChange={onChangeTimeVal}
|
||||||
value={timeVal}
|
value={timeVal}
|
||||||
|
data-test-subj="interval"
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
</EuiFormControlLayout>
|
</EuiFormControlLayout>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EuiBreadcrumb, EuiBetaBadge } from '@elastic/eui';
|
import { EuiBreadcrumb, EuiBetaBadge } from '@elastic/eui';
|
||||||
import React, { memo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import { BetaHeader, ThemedBreadcrumbs } from './styles';
|
import { BetaHeader, ThemedBreadcrumbs } from './styles';
|
||||||
import { useColors } from '../use_colors';
|
import { useColors } from '../use_colors';
|
||||||
|
|
||||||
|
@ -16,6 +16,15 @@ import { useColors } from '../use_colors';
|
||||||
* Breadcrumb menu
|
* Breadcrumb menu
|
||||||
*/
|
*/
|
||||||
export const Breadcrumbs = memo(function ({ breadcrumbs }: { breadcrumbs: EuiBreadcrumb[] }) {
|
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();
|
const { resolverBreadcrumbBackground, resolverEdgeText } = useColors();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -32,7 +41,7 @@ export const Breadcrumbs = memo(function ({ breadcrumbs }: { breadcrumbs: EuiBre
|
||||||
<ThemedBreadcrumbs
|
<ThemedBreadcrumbs
|
||||||
background={resolverBreadcrumbBackground}
|
background={resolverBreadcrumbBackground}
|
||||||
text={resolverEdgeText}
|
text={resolverEdgeText}
|
||||||
breadcrumbs={breadcrumbs}
|
breadcrumbs={crumbsWithLastSubject}
|
||||||
truncate={false}
|
truncate={false}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -42,6 +42,7 @@ import {
|
||||||
} from '../../../../../ingest_manager/common/types/models';
|
} from '../../../../../ingest_manager/common/types/models';
|
||||||
import { createV1SearchResponse, createV2SearchResponse } from './support/test_support';
|
import { createV1SearchResponse, createV2SearchResponse } from './support/test_support';
|
||||||
import { PackageService } from '../../../../../ingest_manager/server/services';
|
import { PackageService } from '../../../../../ingest_manager/server/services';
|
||||||
|
import { metadataTransformPrefix } from '../../../../common/endpoint/constants';
|
||||||
|
|
||||||
describe('test endpoint route', () => {
|
describe('test endpoint route', () => {
|
||||||
let routerMock: jest.Mocked<IRouter>;
|
let routerMock: jest.Mocked<IRouter>;
|
||||||
|
@ -175,7 +176,7 @@ describe('test endpoint route', () => {
|
||||||
type: ElasticsearchAssetType.indexTemplate,
|
type: ElasticsearchAssetType.indexTemplate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0',
|
id: `${metadataTransformPrefix}-0.16.0-dev.0`,
|
||||||
type: ElasticsearchAssetType.transform,
|
type: ElasticsearchAssetType.transform,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
@ -8993,7 +8993,6 @@
|
||||||
"xpack.ingestManager.agentEnrollment.downloadLink": "elastic.co/downloadsに移動",
|
"xpack.ingestManager.agentEnrollment.downloadLink": "elastic.co/downloadsに移動",
|
||||||
"xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "フリートに登録",
|
"xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "フリートに登録",
|
||||||
"xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "スタンドアロンモード",
|
"xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "スタンドアロンモード",
|
||||||
"xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "エージェントを登録する前に、フリートを設定する必要があります。{link}",
|
|
||||||
"xpack.ingestManager.agentEnrollment.flyoutTitle": "エージェントの追加",
|
"xpack.ingestManager.agentEnrollment.flyoutTitle": "エージェントの追加",
|
||||||
"xpack.ingestManager.agentEnrollment.managedDescription": "必要なエージェントの数に関係なく、Fleetでは、簡単に一元的に更新を管理し、エージェントにデプロイすることができます。次の手順に従い、Elasticエージェントをダウンロードし、Fleetに登録してください。",
|
"xpack.ingestManager.agentEnrollment.managedDescription": "必要なエージェントの数に関係なく、Fleetでは、簡単に一元的に更新を管理し、エージェントにデプロイすることができます。次の手順に従い、Elasticエージェントをダウンロードし、Fleetに登録してください。",
|
||||||
"xpack.ingestManager.agentEnrollment.standaloneDescription": "スタンドアロンモードで実行中のエージェントは、構成を変更したい場合には、手動で更新する必要があります。次の手順に従い、スタンドアロンモードでElasticエージェントをダウンロードし、セットアップしてください。",
|
"xpack.ingestManager.agentEnrollment.standaloneDescription": "スタンドアロンモードで実行中のエージェントは、構成を変更したい場合には、手動で更新する必要があります。次の手順に従い、スタンドアロンモードでElasticエージェントをダウンロードし、セットアップしてください。",
|
||||||
|
@ -9075,7 +9074,6 @@
|
||||||
"xpack.ingestManager.alphaMessging.closeFlyoutLabel": "閉じる",
|
"xpack.ingestManager.alphaMessging.closeFlyoutLabel": "閉じる",
|
||||||
"xpack.ingestManager.appNavigation.dataStreamsLinkText": "データセット",
|
"xpack.ingestManager.appNavigation.dataStreamsLinkText": "データセット",
|
||||||
"xpack.ingestManager.appNavigation.epmLinkText": "統合",
|
"xpack.ingestManager.appNavigation.epmLinkText": "統合",
|
||||||
"xpack.ingestManager.appNavigation.fleetLinkText": "フリート",
|
|
||||||
"xpack.ingestManager.appNavigation.overviewLinkText": "概要",
|
"xpack.ingestManager.appNavigation.overviewLinkText": "概要",
|
||||||
"xpack.ingestManager.appNavigation.sendFeedbackButton": "フィードバックを送信",
|
"xpack.ingestManager.appNavigation.sendFeedbackButton": "フィードバックを送信",
|
||||||
"xpack.ingestManager.appNavigation.settingsButton": "設定",
|
"xpack.ingestManager.appNavigation.settingsButton": "設定",
|
||||||
|
@ -9085,9 +9083,6 @@
|
||||||
"xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "すべて",
|
"xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "すべて",
|
||||||
"xpack.ingestManager.breadcrumbs.appTitle": "Ingest Manager",
|
"xpack.ingestManager.breadcrumbs.appTitle": "Ingest Manager",
|
||||||
"xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "データセット",
|
"xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "データセット",
|
||||||
"xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle": "エージェント",
|
|
||||||
"xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle": "登録トークン",
|
|
||||||
"xpack.ingestManager.breadcrumbs.fleetPageTitle": "フリート",
|
|
||||||
"xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "インストール済み",
|
"xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "インストール済み",
|
||||||
"xpack.ingestManager.breadcrumbs.integrationsPageTitle": "統合",
|
"xpack.ingestManager.breadcrumbs.integrationsPageTitle": "統合",
|
||||||
"xpack.ingestManager.breadcrumbs.overviewPageTitle": "概要",
|
"xpack.ingestManager.breadcrumbs.overviewPageTitle": "概要",
|
||||||
|
@ -9156,8 +9151,6 @@
|
||||||
"xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "パッケージが見つかりません",
|
"xpack.ingestManager.epmList.noPackagesFoundPlaceholder": "パッケージが見つかりません",
|
||||||
"xpack.ingestManager.epmList.searchPackagesPlaceholder": "統合を検索",
|
"xpack.ingestManager.epmList.searchPackagesPlaceholder": "統合を検索",
|
||||||
"xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "更新が可能です",
|
"xpack.ingestManager.epmList.updatesAvailableFilterLinkText": "更新が可能です",
|
||||||
"xpack.ingestManager.fleet.pageSubtitle": "構成の更新を管理し、任意のサイズのエージェントのグループにデプロイします。",
|
|
||||||
"xpack.ingestManager.fleet.pageTitle": "フリート",
|
|
||||||
"xpack.ingestManager.genericActionsMenuText": "開く",
|
"xpack.ingestManager.genericActionsMenuText": "開く",
|
||||||
"xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去",
|
"xpack.ingestManager.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去",
|
||||||
"xpack.ingestManager.homeIntegration.tutorialDirectory.ingestManagerAppButtonText": "Ingest Managerベータを試す",
|
"xpack.ingestManager.homeIntegration.tutorialDirectory.ingestManagerAppButtonText": "Ingest Managerベータを試す",
|
||||||
|
@ -9236,7 +9229,6 @@
|
||||||
"xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "エージェントが収集するデータはさまざまなデータセットに整理されます。",
|
"xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "エージェントが収集するデータはさまざまなデータセットに整理されます。",
|
||||||
"xpack.ingestManager.overviewPageEnrollAgentButton": "エージェントの追加",
|
"xpack.ingestManager.overviewPageEnrollAgentButton": "エージェントの追加",
|
||||||
"xpack.ingestManager.overviewPageFleetPanelAction": "エージェントを表示",
|
"xpack.ingestManager.overviewPageFleetPanelAction": "エージェントを表示",
|
||||||
"xpack.ingestManager.overviewPageFleetPanelTitle": "フリート",
|
|
||||||
"xpack.ingestManager.overviewPageFleetPanelTooltip": "Fleetを使用して、中央の場所からエージェントを登録し、構成を管理します。",
|
"xpack.ingestManager.overviewPageFleetPanelTooltip": "Fleetを使用して、中央の場所からエージェントを登録し、構成を管理します。",
|
||||||
"xpack.ingestManager.overviewPageIntegrationsPanelAction": "統合を表示",
|
"xpack.ingestManager.overviewPageIntegrationsPanelAction": "統合を表示",
|
||||||
"xpack.ingestManager.overviewPageIntegrationsPanelTitle": "統合",
|
"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