exists('policyFormErrorsCallout'),
timeline: {
- hasRolloverIndicator: () => exists('timelineHotPhaseRolloverToolTip'),
hasHotPhase: () => exists('ilmTimelineHotPhase'),
hasWarmPhase: () => exists('ilmTimelineWarmPhase'),
hasColdPhase: () => exists('ilmTimelineColdPhase'),
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
index f2266741ec7d1..282daf780b86c 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
@@ -845,14 +845,6 @@ describe('', () => {
expect(actions.timeline.hasColdPhase()).toBe(true);
expect(actions.timeline.hasDeletePhase()).toBe(true);
});
-
- test('show and hide rollover indicator on timeline', async () => {
- const { actions } = testBed;
- expect(actions.timeline.hasRolloverIndicator()).toBe(true);
- await actions.hot.toggleDefaultRollover(false);
- await actions.hot.toggleRollover(false);
- expect(actions.timeline.hasRolloverIndicator()).toBe(false);
- });
});
describe('policy error notifications', () => {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
index 74809965a52d9..c77493476b929 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
@@ -17,7 +17,7 @@ import {
EuiTextColor,
EuiSwitch,
EuiIconTip,
- EuiIcon,
+ EuiText,
} from '@elastic/eui';
import { useFormData, SelectField, NumericField } from '../../../../../../shared_imports';
@@ -68,8 +68,20 @@ export const HotPhase: FunctionComponent = () => {
{' '}
+ defaultMessage="Start writing to a new index when the current index reaches a certain size, document count, or age. Enables you to optimize performance and manage resource usage when working with time series data."
+ />
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescriptionNote',
+ { defaultMessage: 'Note: ' }
+ )}
+
+ {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming}{' '}
{
-
-
- {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming}
-
path={isUsingDefaultRolloverPath}>
{(field) => (
<>
- field.setValue(e.target.checked)}
- data-test-subj="useDefaultRolloverSwitch"
- />
-
-
- }
+
+ field.setValue(e.target.checked)}
+ data-test-subj="useDefaultRolloverSwitch"
+ />
+
+
+
>
)}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
index bbdcbbf4759ef..8cb566ceae25a 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
@@ -45,7 +45,7 @@ export const ForcemergeField: React.FunctionComponent = ({ phase }) => {
<>
{' '}
>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
index 9251b08742476..c85201f708a2b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx
@@ -342,7 +342,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
,
}}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx
index b5fb79811ee2d..8ac387ba106b7 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx
@@ -38,7 +38,7 @@ export const ShrinkField: FunctionComponent = ({ phase }) => {
{' '}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
index 3a9f33fa3d169..62b100b85cbe2 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx
@@ -12,8 +12,8 @@ export const TimelinePhaseText: FunctionComponent<{
phaseName: ReactNode | string;
durationInPhase?: ReactNode | string;
}> = ({ phaseName, durationInPhase }) => (
-
-
+
+
{phaseName}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
index 7d65d2cd6b212..de49e665ed933 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss
@@ -1,11 +1,5 @@
$ilmTimelineBarHeight: $euiSizeS;
-/*
-* For theming we need to shade or tint to get the right color from the base EUI color
-*/
-$ilmDeletePhaseBackgroundColor: tintOrShade($euiColorVis5_behindText, 80%,80%);
-$ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%);
-
.ilmTimeline {
overflow: hidden;
width: 100%;
@@ -49,14 +43,16 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%);
*/
padding: $euiSizeM;
margin-left: $euiSizeM;
- background-color: $ilmDeletePhaseBackgroundColor;
- color: $ilmDeletePhaseColor;
- border-radius: calc(#{$euiSizeS} / 2);
+ background-color: $euiColorLightestShade;
+ color: $euiColorDarkShade;
+ border-radius: 50%;
}
&__colorBar {
display: inline-block;
height: $ilmTimelineBarHeight;
+ margin-top: $euiSizeS;
+ margin-bottom: $euiSizeXS;
border-radius: calc(#{$ilmTimelineBarHeight} / 2);
width: 100%;
}
@@ -84,8 +80,4 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%);
background-color: $euiColorVis1;
}
}
-
- &__rolloverIcon {
- display: inline-block;
- }
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
index 2d83009bd4df4..8097ab51eb59e 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx
@@ -8,14 +8,12 @@
import { i18n } from '@kbn/i18n';
import React, { FunctionComponent, memo } from 'react';
-
-import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiIconTip } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiIconTip } from '@elastic/eui';
import { PhasesExceptDelete } from '../../../../../../common/types';
import {
calculateRelativeFromAbsoluteMilliseconds,
- normalizeTimingsToHumanReadable,
PhaseAgeInMilliseconds,
AbsoluteTimings,
} from '../../lib';
@@ -48,6 +46,12 @@ const msTimeToOverallPercent = (ms: number, totalMs: number) => {
const SCORE_BUFFER_AMOUNT = 50;
const i18nTexts = {
+ title: i18n.translate('xpack.indexLifecycleMgmt.timeline.title', {
+ defaultMessage: 'Policy Summary',
+ }),
+ description: i18n.translate('xpack.indexLifecycleMgmt.timeline.description', {
+ defaultMessage: 'This policy moves data through the following phases.',
+ }),
hotPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle', {
defaultMessage: 'Hot phase',
}),
@@ -69,6 +73,11 @@ const i18nTexts = {
defaultMessage: 'Policy deletes the index after lifecycle phases complete.',
}),
},
+ foreverIcon: {
+ ariaLabel: i18n.translate('xpack.indexLifecycleMgmt.timeline.foreverIconToolTipContent', {
+ defaultMessage: 'Forever',
+ }),
+ },
};
const calculateWidths = (inputs: PhaseAgeInMilliseconds) => {
@@ -118,27 +127,23 @@ export const Timeline: FunctionComponent = memo(
};
const phaseAgeInMilliseconds = calculateRelativeFromAbsoluteMilliseconds(absoluteTimings);
- const humanReadableTimings = normalizeTimingsToHumanReadable(phaseAgeInMilliseconds);
const widths = calculateWidths(phaseAgeInMilliseconds);
const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode =>
phaseAgeInMilliseconds.phases[phase] === Infinity ? (
-
- ) : (
- humanReadableTimings[phase]
- );
+
+ ) : null;
return (
-
- {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', {
- defaultMessage: 'Policy Timeline',
- })}
-
+ {i18nTexts.title}
+
+ {i18nTexts.description}
+
= memo(
>
- {i18nTexts.hotPhase}
-
-
-
-
- >
- ) : (
- i18nTexts.hotPhase
- )
- }
+ phaseName={i18nTexts.hotPhase}
durationInPhase={getDurationInPhaseContent('hot')}
/>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
index 5deba8607cd52..3923cf93cd0d3 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
@@ -16,7 +16,7 @@ export const i18nTexts = {
'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescription',
{
defaultMessage:
- 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.',
+ 'How long it takes to reach the rollover criteria in the hot phase can vary.',
}
),
searchableSnapshotInHotPhase: {
@@ -195,7 +195,7 @@ export const i18nTexts = {
descriptions: {
hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescription', {
defaultMessage:
- 'This phase is required. You are actively querying and writing to your index. For faster updates, you can roll over the index when it gets too big or too old.',
+ 'You actively store and query data in the hot phase. All policies have a hot phase.',
}),
warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescription', {
defaultMessage:
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
index 7ec20cc2a5966..8a9635e2db219 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
@@ -11,7 +11,6 @@ import { deserializer } from '../form';
import {
formDataToAbsoluteTimings,
calculateRelativeFromAbsoluteMilliseconds,
- absoluteTimingToRelativeTiming,
} from './absolute_timing_to_relative_timing';
export const calculateRelativeTimingMs = flow(
@@ -273,243 +272,4 @@ describe('Conversion of absolute policy timing to relative timing', () => {
});
});
});
-
- describe('absoluteTimingToRelativeTiming', () => {
- describe('policy that never deletes data (keep forever)', () => {
- test('always hot', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- },
- })
- )
- ).toEqual({ total: 'forever', hot: 'forever', warm: undefined, cold: undefined });
- });
-
- test('hot, then always warm', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- warm: {
- actions: {},
- },
- },
- })
- )
- ).toEqual({ total: 'forever', hot: 'less than a day', warm: 'forever', cold: undefined });
- });
-
- test('hot, then warm, then always cold', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- warm: {
- min_age: '1M',
- actions: {},
- },
- cold: {
- min_age: '34d',
- actions: {},
- },
- },
- })
- )
- ).toEqual({
- total: 'forever',
- hot: '30 days',
- warm: '4 days',
- cold: 'forever',
- });
- });
-
- test('hot, then always cold', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- cold: {
- min_age: '34d',
- actions: {},
- },
- },
- })
- )
- ).toEqual({ total: 'forever', hot: '34 days', warm: undefined, cold: 'forever' });
- });
- });
-
- describe('policy that deletes data', () => {
- test('hot, then delete', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- delete: {
- min_age: '1M',
- actions: {},
- },
- },
- })
- )
- ).toEqual({
- total: '30 days',
- hot: '30 days',
- warm: undefined,
- cold: undefined,
- });
- });
-
- test('hot, then warm, then delete', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- warm: {
- min_age: '24d',
- actions: {},
- },
- delete: {
- min_age: '1M',
- actions: {},
- },
- },
- })
- )
- ).toEqual({
- total: '30 days',
- hot: '24 days',
- warm: '6 days',
- cold: undefined,
- });
- });
-
- test('hot, then warm, then cold, then delete', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- warm: {
- min_age: '24d',
- actions: {},
- },
- cold: {
- min_age: '2M',
- actions: {},
- },
- delete: {
- min_age: '2d',
- actions: {},
- },
- },
- })
- )
- ).toEqual({
- total: '61 days',
- hot: '24 days',
- warm: '37 days',
- cold: 'less than a day',
- });
- });
-
- test('hot, then cold, then delete', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- cold: {
- min_age: '2M',
- actions: {},
- },
- delete: {
- min_age: '2d',
- actions: {},
- },
- },
- })
- )
- ).toEqual({
- total: '61 days',
- hot: '61 days',
- warm: undefined,
- cold: 'less than a day',
- });
- });
-
- test('hot, then long warm, then short cold, then delete', () => {
- expect(
- absoluteTimingToRelativeTiming(
- deserializer({
- name: 'test',
- phases: {
- hot: {
- min_age: '0ms',
- actions: {},
- },
- warm: {
- min_age: '2M',
- actions: {},
- },
- cold: {
- min_age: '1d',
- actions: {},
- },
- delete: {
- min_age: '2d',
- actions: {},
- },
- },
- })
- )
- ).toEqual({
- total: '61 days',
- hot: '61 days',
- warm: 'less than a day',
- cold: 'less than a day',
- });
- });
- });
- });
});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
index 73ff8c76b9233..2974a88c22343 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
@@ -21,8 +21,6 @@
*/
import moment from 'moment';
-import { i18n } from '@kbn/i18n';
-import { flow } from 'fp-ts/function';
import { splitSizeAndUnits } from '../../../lib/policies';
@@ -34,21 +32,6 @@ type MinAgePhase = 'warm' | 'cold' | 'delete';
type Phase = 'hot' | MinAgePhase;
-const i18nTexts = {
- forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.forever', {
- defaultMessage: 'forever',
- }),
- lessThanADay: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.lessThanADay', {
- defaultMessage: 'less than a day',
- }),
- day: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.day', {
- defaultMessage: 'day',
- }),
- days: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.days', {
- defaultMessage: 'days',
- }),
-};
-
const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete'];
const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({
@@ -162,38 +145,3 @@ export const calculateRelativeFromAbsoluteMilliseconds = (
};
export type RelativePhaseTimingInMs = ReturnType;
-
-const millisecondsToDays = (milliseconds?: number): string | undefined => {
- if (milliseconds == null) {
- return;
- }
- if (!isFinite(milliseconds)) {
- return i18nTexts.forever;
- }
- const days = milliseconds / 8.64e7;
- return days < 1
- ? i18nTexts.lessThanADay
- : `${Math.floor(days)} ${days === 1 ? i18nTexts.day : i18nTexts.days}`;
-};
-
-export const normalizeTimingsToHumanReadable = ({
- total,
- phases,
-}: PhaseAgeInMilliseconds): { total?: string; hot?: string; warm?: string; cold?: string } => {
- return {
- total: millisecondsToDays(total),
- hot: millisecondsToDays(phases.hot),
- warm: millisecondsToDays(phases.warm),
- cold: millisecondsToDays(phases.cold),
- };
-};
-
-/**
- * Given {@link FormInternal}, extract the min_age values for each phase and calculate
- * human readable strings for communicating how long data will remain in a phase.
- */
-export const absoluteTimingToRelativeTiming = flow(
- formDataToAbsoluteTimings,
- calculateRelativeFromAbsoluteMilliseconds,
- normalizeTimingsToHumanReadable
-);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
index 396318a1d78cf..af4757a7b7105 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
@@ -6,9 +6,7 @@
*/
export {
- absoluteTimingToRelativeTiming,
calculateRelativeFromAbsoluteMilliseconds,
- normalizeTimingsToHumanReadable,
formDataToAbsoluteTimings,
AbsoluteTimings,
PhaseAgeInMilliseconds,
diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json
new file mode 100644
index 0000000000000..73dcc62132cbf
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "__jest__/**/*",
+ "common/**/*",
+ "public/**/*",
+ "server/**/*",
+ "../../typings/**/*",
+ ],
+ "references": [
+ { "path": "../../../src/core/tsconfig.json" },
+ // required plugins
+ { "path": "../licensing/tsconfig.json" },
+ { "path": "../../../src/plugins/management/tsconfig.json" },
+ { "path": "../features/tsconfig.json" },
+ { "path": "../../../src/plugins/share/tsconfig.json" },
+ // optional plugins
+ { "path": "../cloud/tsconfig.json" },
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../index_management/tsconfig.json" },
+ { "path": "../../../src/plugins/home/tsconfig.json" },
+ // required bundles
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ ]
+}
diff --git a/x-pack/plugins/infra/common/alerting/metrics/index.ts b/x-pack/plugins/infra/common/alerting/metrics/index.ts
index 5151a40c7e8b1..2c66638711cd0 100644
--- a/x-pack/plugins/infra/common/alerting/metrics/index.ts
+++ b/x-pack/plugins/infra/common/alerting/metrics/index.ts
@@ -10,8 +10,8 @@ export const INFRA_ALERT_PREVIEW_PATH = '/api/infra/alerting/preview';
export const TOO_MANY_BUCKETS_PREVIEW_EXCEPTION = 'TOO_MANY_BUCKETS_PREVIEW_EXCEPTION';
export interface TooManyBucketsPreviewExceptionMetadata {
- TOO_MANY_BUCKETS_PREVIEW_EXCEPTION: any;
- maxBuckets: number;
+ TOO_MANY_BUCKETS_PREVIEW_EXCEPTION: boolean;
+ maxBuckets: any;
}
export const isTooManyBucketsPreviewException = (
value: any
diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts
index 47a5202cc7275..7a4edb8f49189 100644
--- a/x-pack/plugins/infra/common/alerting/metrics/types.ts
+++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts
@@ -4,14 +4,15 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import * as rt from 'io-ts';
+import { ANOMALY_THRESHOLD } from '../../infra_ml';
import { ItemTypeRT } from '../../inventory_models/types';
// TODO: Have threshold and inventory alerts import these types from this file instead of from their
// local directories
export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
+export const METRIC_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly';
export enum Comparator {
GT = '>',
@@ -34,6 +35,26 @@ export enum Aggregators {
P99 = 'p99',
}
+const metricAnomalyNodeTypeRT = rt.union([rt.literal('hosts'), rt.literal('k8s')]);
+const metricAnomalyMetricRT = rt.union([
+ rt.literal('memory_usage'),
+ rt.literal('network_in'),
+ rt.literal('network_out'),
+]);
+const metricAnomalyInfluencerFilterRT = rt.type({
+ fieldName: rt.string,
+ fieldValue: rt.string,
+});
+
+export interface MetricAnomalyParams {
+ nodeType: rt.TypeOf;
+ metric: rt.TypeOf;
+ alertInterval?: string;
+ sourceId?: string;
+ threshold: Exclude;
+ influencerFilter: rt.TypeOf | undefined;
+}
+
// Alert Preview API
const baseAlertRequestParamsRT = rt.intersection([
rt.partial({
@@ -51,7 +72,6 @@ const baseAlertRequestParamsRT = rt.intersection([
rt.literal('M'),
rt.literal('y'),
]),
- criteria: rt.array(rt.any),
alertInterval: rt.string,
alertThrottle: rt.string,
alertOnNoData: rt.boolean,
@@ -65,6 +85,7 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([
}),
rt.type({
alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID),
+ criteria: rt.array(rt.any),
}),
]);
export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf<
@@ -76,26 +97,49 @@ const inventoryAlertPreviewRequestParamsRT = rt.intersection([
rt.type({
nodeType: ItemTypeRT,
alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID),
+ criteria: rt.array(rt.any),
}),
]);
export type InventoryAlertPreviewRequestParams = rt.TypeOf<
typeof inventoryAlertPreviewRequestParamsRT
>;
+const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([
+ baseAlertRequestParamsRT,
+ rt.type({
+ nodeType: metricAnomalyNodeTypeRT,
+ metric: metricAnomalyMetricRT,
+ threshold: rt.number,
+ alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID),
+ }),
+ rt.partial({
+ influencerFilter: metricAnomalyInfluencerFilterRT,
+ }),
+]);
+export type MetricAnomalyAlertPreviewRequestParams = rt.TypeOf<
+ typeof metricAnomalyAlertPreviewRequestParamsRT
+>;
+
export const alertPreviewRequestParamsRT = rt.union([
metricThresholdAlertPreviewRequestParamsRT,
inventoryAlertPreviewRequestParamsRT,
+ metricAnomalyAlertPreviewRequestParamsRT,
]);
export type AlertPreviewRequestParams = rt.TypeOf;
export const alertPreviewSuccessResponsePayloadRT = rt.type({
numberOfGroups: rt.number,
- resultTotals: rt.type({
- fired: rt.number,
- noData: rt.number,
- error: rt.number,
- notifications: rt.number,
- }),
+ resultTotals: rt.intersection([
+ rt.type({
+ fired: rt.number,
+ noData: rt.number,
+ error: rt.number,
+ notifications: rt.number,
+ }),
+ rt.partial({
+ warning: rt.number,
+ }),
+ ]),
});
export type AlertPreviewSuccessResponsePayload = rt.TypeOf<
typeof alertPreviewSuccessResponsePayloadRT
diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts
index 27574d01be898..0b70b65b7069e 100644
--- a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts
+++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts
@@ -62,6 +62,7 @@ export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({
rt.type({
// the ID of the source configuration
sourceId: rt.string,
+ anomalyThreshold: rt.number,
// the time range to fetch the log entry anomalies from
timeRange: timeRangeRT,
}),
diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts
index 3c2615a447b07..3ee6189dcbf9a 100644
--- a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts
+++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts
@@ -62,6 +62,7 @@ export const getMetricsK8sAnomaliesRequestPayloadRT = rt.type({
rt.type({
// the ID of the source configuration
sourceId: rt.string,
+ anomalyThreshold: rt.number,
// the time range to fetch the log entry anomalies from
timeRange: timeRangeRT,
}),
diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts
index d50495689e9d8..23c2ce5f0c21f 100644
--- a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts
+++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts
@@ -9,7 +9,6 @@ export * from './log_entry_categories';
export * from './log_entry_category_datasets';
export * from './log_entry_category_datasets_stats';
export * from './log_entry_category_examples';
-export * from './log_entry_rate';
export * from './log_entry_examples';
export * from './log_entry_anomalies';
export * from './log_entry_anomalies_datasets';
diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts
deleted file mode 100644
index 943e1df70c0ba..0000000000000
--- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts
+++ /dev/null
@@ -1,86 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import * as rt from 'io-ts';
-
-import { badRequestErrorRT, conflictErrorRT, forbiddenErrorRT, timeRangeRT } from '../../shared';
-
-export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH =
- '/api/infra/log_analysis/results/log_entry_rate';
-
-/**
- * request
- */
-
-export const getLogEntryRateRequestPayloadRT = rt.type({
- data: rt.intersection([
- rt.type({
- bucketDuration: rt.number,
- sourceId: rt.string,
- timeRange: timeRangeRT,
- }),
- rt.partial({
- datasets: rt.array(rt.string),
- }),
- ]),
-});
-
-export type GetLogEntryRateRequestPayload = rt.TypeOf;
-
-/**
- * response
- */
-
-export const logEntryRateAnomalyRT = rt.type({
- id: rt.string,
- actualLogEntryRate: rt.number,
- anomalyScore: rt.number,
- duration: rt.number,
- startTime: rt.number,
- typicalLogEntryRate: rt.number,
-});
-
-export type LogEntryRateAnomaly = rt.TypeOf;
-
-export const logEntryRatePartitionRT = rt.type({
- analysisBucketCount: rt.number,
- anomalies: rt.array(logEntryRateAnomalyRT),
- averageActualLogEntryRate: rt.number,
- maximumAnomalyScore: rt.number,
- numberOfLogEntries: rt.number,
- partitionId: rt.string,
-});
-
-export type LogEntryRatePartition = rt.TypeOf;
-
-export const logEntryRateHistogramBucketRT = rt.type({
- partitions: rt.array(logEntryRatePartitionRT),
- startTime: rt.number,
-});
-
-export type LogEntryRateHistogramBucket = rt.TypeOf;
-
-export const getLogEntryRateSuccessReponsePayloadRT = rt.type({
- data: rt.type({
- bucketDuration: rt.number,
- histogramBuckets: rt.array(logEntryRateHistogramBucketRT),
- totalNumberOfLogEntries: rt.number,
- }),
-});
-
-export type GetLogEntryRateSuccessResponsePayload = rt.TypeOf<
- typeof getLogEntryRateSuccessReponsePayloadRT
->;
-
-export const getLogEntryRateResponsePayloadRT = rt.union([
- getLogEntryRateSuccessReponsePayloadRT,
- badRequestErrorRT,
- conflictErrorRT,
- forbiddenErrorRT,
-]);
-
-export type GetLogEntryRateReponsePayload = rt.TypeOf;
diff --git a/x-pack/plugins/infra/common/http_api/source_api.ts b/x-pack/plugins/infra/common/http_api/source_api.ts
index 257383be859aa..f14151531ba35 100644
--- a/x-pack/plugins/infra/common/http_api/source_api.ts
+++ b/x-pack/plugins/infra/common/http_api/source_api.ts
@@ -90,6 +90,7 @@ export const SavedSourceConfigurationRuntimeType = rt.partial({
metricsExplorerDefaultView: rt.string,
fields: SavedSourceConfigurationFieldsRuntimeType,
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
+ anomalyThreshold: rt.number,
});
export interface InfraSavedSourceConfiguration
@@ -107,6 +108,7 @@ export const pickSavedSourceConfiguration = (
inventoryDefaultView,
metricsExplorerDefaultView,
logColumns,
+ anomalyThreshold,
} = value;
const { container, host, pod, tiebreaker, timestamp } = fields;
@@ -119,6 +121,7 @@ export const pickSavedSourceConfiguration = (
metricsExplorerDefaultView,
fields: { container, host, pod, tiebreaker, timestamp },
logColumns,
+ anomalyThreshold,
};
};
@@ -140,6 +143,7 @@ export const StaticSourceConfigurationRuntimeType = rt.partial({
metricsExplorerDefaultView: rt.string,
fields: StaticSourceConfigurationFieldsRuntimeType,
logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType),
+ anomalyThreshold: rt.number,
});
export interface InfraStaticSourceConfiguration
diff --git a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts
index 589e57a1388b5..81e46d85ba220 100644
--- a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts
+++ b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts
@@ -5,36 +5,44 @@
* 2.0.
*/
-export const ML_SEVERITY_SCORES = {
- warning: 3,
- minor: 25,
- major: 50,
- critical: 75,
-};
+export enum ANOMALY_SEVERITY {
+ CRITICAL = 'critical',
+ MAJOR = 'major',
+ MINOR = 'minor',
+ WARNING = 'warning',
+ LOW = 'low',
+ UNKNOWN = 'unknown',
+}
-export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES;
+export enum ANOMALY_THRESHOLD {
+ CRITICAL = 75,
+ MAJOR = 50,
+ MINOR = 25,
+ WARNING = 3,
+ LOW = 0,
+}
-export const ML_SEVERITY_COLORS = {
- critical: 'rgb(228, 72, 72)',
- major: 'rgb(229, 113, 0)',
- minor: 'rgb(255, 221, 0)',
- warning: 'rgb(125, 180, 226)',
+export const SEVERITY_COLORS = {
+ CRITICAL: '#fe5050',
+ MAJOR: '#fba740',
+ MINOR: '#fdec25',
+ WARNING: '#8bc8fb',
+ LOW: '#d2e9f7',
+ BLANK: '#ffffff',
};
-export const getSeverityCategoryForScore = (
- score: number
-): MLSeverityScoreCategories | undefined => {
- if (score >= ML_SEVERITY_SCORES.critical) {
- return 'critical';
- } else if (score >= ML_SEVERITY_SCORES.major) {
- return 'major';
- } else if (score >= ML_SEVERITY_SCORES.minor) {
- return 'minor';
- } else if (score >= ML_SEVERITY_SCORES.warning) {
- return 'warning';
+export const getSeverityCategoryForScore = (score: number): ANOMALY_SEVERITY | undefined => {
+ if (score >= ANOMALY_THRESHOLD.CRITICAL) {
+ return ANOMALY_SEVERITY.CRITICAL;
+ } else if (score >= ANOMALY_THRESHOLD.MAJOR) {
+ return ANOMALY_SEVERITY.MAJOR;
+ } else if (score >= ANOMALY_THRESHOLD.MINOR) {
+ return ANOMALY_SEVERITY.MINOR;
+ } else if (score >= ANOMALY_THRESHOLD.WARNING) {
+ return ANOMALY_SEVERITY.WARNING;
} else {
// Category is too low to include
- return undefined;
+ return ANOMALY_SEVERITY.LOW;
}
};
diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts
index f460747f8b142..113e8ff8c34e6 100644
--- a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts
+++ b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts
@@ -40,10 +40,6 @@ export const getSeverityCategoryForScore = (
}
};
-export const formatAnomalyScore = (score: number) => {
- return Math.round(score);
-};
-
export const formatOneDecimalPlace = (number: number) => {
return Math.round(number * 10) / 10;
};
diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json
index 327cb674de00b..c892f7017da33 100644
--- a/x-pack/plugins/infra/kibana.json
+++ b/x-pack/plugins/infra/kibana.json
@@ -13,9 +13,17 @@
"alerts",
"triggersActionsUi"
],
- "optionalPlugins": ["ml", "observability", "home"],
+ "optionalPlugins": ["ml", "observability", "home", "embeddable"],
"server": true,
"ui": true,
"configPath": ["xpack", "infra"],
- "requiredBundles": ["observability", "licenseManagement", "kibanaUtils", "kibanaReact", "home"]
+ "requiredBundles": [
+ "observability",
+ "licenseManagement",
+ "kibanaUtils",
+ "kibanaReact",
+ "home",
+ "ml",
+ "embeddable"
+ ]
}
diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx
index cfe1579b5f408..57c6f695453ef 100644
--- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx
+++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx
@@ -37,7 +37,7 @@ interface Props {
alertInterval: string;
alertThrottle: string;
alertType: PreviewableAlertTypes;
- alertParams: { criteria: any[]; sourceId: string } & Record;
+ alertParams: { criteria?: any[]; sourceId: string } & Record;
validate: (params: any) => ValidationResult;
showNoDataResults?: boolean;
groupByDisplayName?: string;
@@ -109,6 +109,7 @@ export const AlertPreview: React.FC = (props) => {
}, [previewLookbackInterval, alertInterval]);
const isPreviewDisabled = useMemo(() => {
+ if (!alertParams.criteria) return false;
const validationResult = validate({ criteria: alertParams.criteria } as any);
const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
@@ -123,6 +124,11 @@ export const AlertPreview: React.FC = (props) => {
return unthrottledNotifications > notifications;
}, [previewResult, showNoDataResults]);
+ const hasWarningThreshold = useMemo(
+ () => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')) ?? false,
+ [alertParams]
+ );
+
return (
= (props) => {
-
-
-
- ),
- }}
- />{' '}
- {previewResult.groupByDisplayName ? (
- <>
- {' '}
-
-
- {' '}
- >
- ) : null}
- e.value === previewResult.previewLookbackInterval
- )?.shortText,
- }}
- />
- >
+
}
>
{showNoDataResults && previewResult.resultTotals.noData ? (
+ ),
boldedResultsNumber: (
{i18n.translate(
'xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber',
{
- defaultMessage:
- '{noData, plural, one {was # result} other {were # results}}',
+ defaultMessage: '{noData, plural, one {# result} other {# results}}',
values: {
noData: previewResult.resultTotals.noData,
},
@@ -361,6 +333,145 @@ export const AlertPreview: React.FC = (props) => {
);
};
+const PreviewTextString = ({
+ previewResult,
+ hasWarningThreshold,
+}: {
+ previewResult: AlertPreviewSuccessResponsePayload & Record;
+ hasWarningThreshold: boolean;
+}) => {
+ const instanceCount = hasWarningThreshold ? (
+
+ ),
+ criticalInstances: (
+
+
+
+ ),
+ warningInstances: (
+
+
+
+ ),
+ boldCritical: (
+
+
+
+ ),
+ boldWarning: (
+
+
+
+ ),
+ }}
+ />
+ ) : (
+
+ ),
+ firedTimes: (
+
+
+
+ ),
+ }}
+ />
+ );
+
+ const groupByText = previewResult.groupByDisplayName ? (
+ <>
+
+
+
+ ),
+ }}
+ />{' '}
+ >
+ ) : (
+ <>>
+ );
+
+ const lookbackText = (
+ e.value === previewResult.previewLookbackInterval)
+ ?.shortText,
+ }}
+ />
+ );
+
+ return (
+
+ );
+};
+
const previewOptions = [
{
value: 'h',
diff --git a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts
index a1cee1361a18f..2bb98e83cbe70 100644
--- a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts
+++ b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts
@@ -10,13 +10,15 @@ import {
INFRA_ALERT_PREVIEW_PATH,
METRIC_THRESHOLD_ALERT_TYPE_ID,
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
+ METRIC_ANOMALY_ALERT_TYPE_ID,
AlertPreviewRequestParams,
AlertPreviewSuccessResponsePayload,
} from '../../../../common/alerting/metrics';
export type PreviewableAlertTypes =
| typeof METRIC_THRESHOLD_ALERT_TYPE_ID
- | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID;
+ | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID
+ | typeof METRIC_ANOMALY_ALERT_TYPE_ID;
export async function getAlertPreview({
fetch,
diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
new file mode 100644
index 0000000000000..f1236c4fc2c2b
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
@@ -0,0 +1,151 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import React, { useState, useCallback, useMemo } from 'react';
+import {
+ EuiPopover,
+ EuiButtonEmpty,
+ EuiContextMenu,
+ EuiContextMenuPanelDescriptor,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities';
+import { PrefilledInventoryAlertFlyout } from '../../inventory/components/alert_flyout';
+import { PrefilledThresholdAlertFlyout } from '../../metric_threshold/components/alert_flyout';
+import { PrefilledAnomalyAlertFlyout } from '../../metric_anomaly/components/alert_flyout';
+import { useLinkProps } from '../../../hooks/use_link_props';
+
+type VisibleFlyoutType = 'inventory' | 'threshold' | 'anomaly' | null;
+
+export const MetricsAlertDropdown = () => {
+ const [popoverOpen, setPopoverOpen] = useState(false);
+ const [visibleFlyoutType, setVisibleFlyoutType] = useState(null);
+ const { hasInfraMLCapabilities } = useInfraMLCapabilities();
+
+ const closeFlyout = useCallback(() => setVisibleFlyoutType(null), [setVisibleFlyoutType]);
+
+ const manageAlertsLinkProps = useLinkProps({
+ app: 'management',
+ pathname: '/insightsAndAlerting/triggersActions/alerts',
+ });
+
+ const panels: EuiContextMenuPanelDescriptor[] = useMemo(
+ () => [
+ {
+ id: 0,
+ title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', {
+ defaultMessage: 'Alerts',
+ }),
+ items: [
+ {
+ name: i18n.translate('xpack.infra.alerting.infrastructureDropdownMenu', {
+ defaultMessage: 'Infrastructure',
+ }),
+ panel: 1,
+ },
+ {
+ name: i18n.translate('xpack.infra.alerting.metricsDropdownMenu', {
+ defaultMessage: 'Metrics',
+ }),
+ panel: 2,
+ },
+ {
+ name: i18n.translate('xpack.infra.alerting.manageAlerts', {
+ defaultMessage: 'Manage alerts',
+ }),
+ icon: 'tableOfContents',
+ onClick: manageAlertsLinkProps.onClick,
+ },
+ ],
+ },
+ {
+ id: 1,
+ title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', {
+ defaultMessage: 'Infrastructure alerts',
+ }),
+ items: [
+ {
+ name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', {
+ defaultMessage: 'Create inventory alert',
+ }),
+ onClick: () => setVisibleFlyoutType('inventory'),
+ },
+ ].concat(
+ hasInfraMLCapabilities
+ ? {
+ name: i18n.translate('xpack.infra.alerting.createAnomalyAlertButton', {
+ defaultMessage: 'Create anomaly alert',
+ }),
+ onClick: () => setVisibleFlyoutType('anomaly'),
+ }
+ : []
+ ),
+ },
+ {
+ id: 2,
+ title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', {
+ defaultMessage: 'Metrics alerts',
+ }),
+ items: [
+ {
+ name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', {
+ defaultMessage: 'Create threshold alert',
+ }),
+ onClick: () => setVisibleFlyoutType('threshold'),
+ },
+ ],
+ },
+ ],
+ [manageAlertsLinkProps, setVisibleFlyoutType, hasInfraMLCapabilities]
+ );
+
+ const closePopover = useCallback(() => {
+ setPopoverOpen(false);
+ }, [setPopoverOpen]);
+
+ const openPopover = useCallback(() => {
+ setPopoverOpen(true);
+ }, [setPopoverOpen]);
+
+ return (
+ <>
+
+
+
+ }
+ isOpen={popoverOpen}
+ closePopover={closePopover}
+ >
+
+
+
+ >
+ );
+};
+
+interface AlertFlyoutProps {
+ visibleFlyoutType: VisibleFlyoutType;
+ onClose(): void;
+}
+
+const AlertFlyout = ({ visibleFlyoutType, onClose }: AlertFlyoutProps) => {
+ switch (visibleFlyoutType) {
+ case 'inventory':
+ return ;
+ case 'threshold':
+ return ;
+ case 'anomaly':
+ return ;
+ default:
+ return null;
+ }
+};
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx
deleted file mode 100644
index a7b6c9fb7104c..0000000000000
--- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx
+++ /dev/null
@@ -1,59 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useState, useCallback } from 'react';
-import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
-import { AlertFlyout } from './alert_flyout';
-import { ManageAlertsContextMenuItem } from './manage_alerts_context_menu_item';
-
-export const InventoryAlertDropdown = () => {
- const [popoverOpen, setPopoverOpen] = useState(false);
- const [flyoutVisible, setFlyoutVisible] = useState(false);
-
- const { inventoryPrefill } = useAlertPrefillContext();
- const { nodeType, metric, filterQuery } = inventoryPrefill;
-
- const closePopover = useCallback(() => {
- setPopoverOpen(false);
- }, [setPopoverOpen]);
-
- const openPopover = useCallback(() => {
- setPopoverOpen(true);
- }, [setPopoverOpen]);
-
- const menuItems = [
- setFlyoutVisible(true)}>
-
- ,
- ,
- ];
-
- return (
- <>
-
-
-
- }
- isOpen={popoverOpen}
- closePopover={closePopover}
- >
-
-
-
- >
- );
-};
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
index 815e1f2be33f2..33fe3c7af30c7 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx
@@ -8,8 +8,7 @@
import React, { useCallback, useContext, useMemo } from 'react';
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
+import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
import { InfraWaffleMapOptions } from '../../../lib/lib';
import { InventoryItemType } from '../../../../common/inventory_models/types';
import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
@@ -49,3 +48,18 @@ export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }:
return <>{visible && AddAlertFlyout}>;
};
+
+export const PrefilledInventoryAlertFlyout = ({ onClose }: { onClose(): void }) => {
+ const { inventoryPrefill } = useAlertPrefillContext();
+ const { nodeType, metric, filterQuery } = inventoryPrefill;
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
index d403c254f2bd0..4a05521e9fc87 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { debounce, pick } from 'lodash';
+import { debounce, pick, omit } from 'lodash';
import { Unit } from '@elastic/datemath';
import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react';
import { IFieldType } from 'src/plugins/data/public';
@@ -21,6 +21,7 @@ import {
EuiCheckbox,
EuiToolTip,
EuiIcon,
+ EuiHealth,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -423,9 +424,24 @@ const StyledExpression = euiStyled.div`
padding: 0 4px;
`;
+const StyledHealth = euiStyled(EuiHealth)`
+ margin-left: 4px;
+`;
+
export const ExpressionRow: React.FC = (props) => {
const { setAlertParams, expression, errors, expressionId, remove, canDelete, fields } = props;
- const { metric, comparator = Comparator.GT, threshold = [], customMetric } = expression;
+ const {
+ metric,
+ comparator = Comparator.GT,
+ threshold = [],
+ customMetric,
+ warningThreshold = [],
+ warningComparator,
+ } = expression;
+
+ const [displayWarningThreshold, setDisplayWarningThreshold] = useState(
+ Boolean(warningThreshold?.length)
+ );
const updateMetric = useCallback(
(m?: SnapshotMetricType | string) => {
@@ -452,6 +468,13 @@ export const ExpressionRow: React.FC = (props) => {
[expressionId, expression, setAlertParams]
);
+ const updateWarningComparator = useCallback(
+ (c?: string) => {
+ setAlertParams(expressionId, { ...expression, warningComparator: c as Comparator });
+ },
+ [expressionId, expression, setAlertParams]
+ );
+
const updateThreshold = useCallback(
(t) => {
if (t.join() !== expression.threshold.join()) {
@@ -461,6 +484,58 @@ export const ExpressionRow: React.FC = (props) => {
[expressionId, expression, setAlertParams]
);
+ const updateWarningThreshold = useCallback(
+ (t) => {
+ if (t.join() !== expression.warningThreshold?.join()) {
+ setAlertParams(expressionId, { ...expression, warningThreshold: t });
+ }
+ },
+ [expressionId, expression, setAlertParams]
+ );
+
+ const toggleWarningThreshold = useCallback(() => {
+ if (!displayWarningThreshold) {
+ setDisplayWarningThreshold(true);
+ setAlertParams(expressionId, {
+ ...expression,
+ warningComparator: comparator,
+ warningThreshold: [],
+ });
+ } else {
+ setDisplayWarningThreshold(false);
+ setAlertParams(expressionId, omit(expression, 'warningComparator', 'warningThreshold'));
+ }
+ }, [
+ displayWarningThreshold,
+ setDisplayWarningThreshold,
+ setAlertParams,
+ comparator,
+ expression,
+ expressionId,
+ ]);
+
+ const criticalThresholdExpression = (
+
+ );
+
+ const warningThresholdExpression = displayWarningThreshold && (
+
+ );
+
const ofFields = useMemo(() => {
let myMetrics = hostMetricTypes;
@@ -515,25 +590,62 @@ export const ExpressionRow: React.FC = (props) => {
fields={fields}
/>
-
-
-
- {metric && (
-
- {metricUnit[metric]?.label || ''}
-
- )}
+ {!displayWarningThreshold && criticalThresholdExpression}
+ {displayWarningThreshold && (
+ <>
+
+ {criticalThresholdExpression}
+
+
+
+
+
+ {warningThresholdExpression}
+
+
+
+
+
+ >
+ )}
+ {!displayWarningThreshold && (
+ <>
+ {' '}
+
+
+
+
+
+
+ >
+ )}
{canDelete && (
@@ -553,6 +665,38 @@ export const ExpressionRow: React.FC = (props) => {
);
};
+const ThresholdElement: React.FC<{
+ updateComparator: (c?: string) => void;
+ updateThreshold: (t?: number[]) => void;
+ threshold: InventoryMetricConditions['threshold'];
+ comparator: InventoryMetricConditions['comparator'];
+ errors: IErrorObject;
+ metric?: SnapshotMetricType;
+}> = ({ updateComparator, updateThreshold, threshold, metric, comparator, errors }) => {
+ return (
+ <>
+
+
+
+ {metric && (
+
+ {metricUnit[metric]?.label || ''}
+
+ )}
+ >
+ );
+};
+
const getDisplayNameForType = (type: InventoryItemType) => {
const inventoryModel = findInventoryModel(type);
return inventoryModel.displayName;
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx
index f02f98c49f01a..bd7812acac678 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/node_type.tsx
@@ -68,7 +68,7 @@ export const NodeTypeExpression = ({
setAggTypePopoverOpen(false)}>
{
- if (!isNumber(v)) {
- const key = i === 0 ? 'threshold0' : 'threshold1';
- errors[id][key].push(
- i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', {
- defaultMessage: 'Thresholds must contain a valid number.',
- })
- );
- }
- });
- }
-
- if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) {
- errors[id].threshold1.push(
+ if (c.warningThreshold && !c.warningThreshold.length) {
+ errors[id].warning.threshold0.push(
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
defaultMessage: 'Threshold is required.',
})
);
}
+ for (const props of [
+ { comparator: c.comparator, threshold: c.threshold, type: 'critical' },
+ { comparator: c.warningComparator, threshold: c.warningThreshold, type: 'warning' },
+ ]) {
+ // The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i].
+ // We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element.
+ const { comparator, threshold, type } = props as {
+ comparator?: Comparator;
+ threshold?: number[];
+ type: 'critical' | 'warning';
+ };
+ if (threshold && threshold.length && ![...threshold].every(isNumber)) {
+ [...threshold].forEach((v, i) => {
+ if (!isNumber(v)) {
+ const key = i === 0 ? 'threshold0' : 'threshold1';
+ errors[id][type][key].push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', {
+ defaultMessage: 'Thresholds must contain a valid number.',
+ })
+ );
+ }
+ });
+ }
+
+ if (comparator === Comparator.BETWEEN && (!threshold || threshold.length < 2)) {
+ errors[id][type].threshold1.push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
+ defaultMessage: 'Threshold is required.',
+ })
+ );
+ }
+ }
if (!c.timeSize) {
errors[id].timeWindowSize.push(
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx
new file mode 100644
index 0000000000000..9d467e1df7e36
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/alert_flyout.tsx
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useContext, useMemo } from 'react';
+
+import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
+import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
+import { InfraWaffleMapOptions } from '../../../lib/lib';
+import { InventoryItemType } from '../../../../common/inventory_models/types';
+import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
+
+interface Props {
+ visible?: boolean;
+ metric?: InfraWaffleMapOptions['metric'];
+ nodeType?: InventoryItemType;
+ filter?: string;
+ setVisible(val: boolean): void;
+}
+
+export const AlertFlyout = ({ metric, nodeType, visible, setVisible }: Props) => {
+ const { triggersActionsUI } = useContext(TriggerActionsContext);
+
+ const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]);
+ const AddAlertFlyout = useMemo(
+ () =>
+ triggersActionsUI &&
+ triggersActionsUI.getAddAlertFlyout({
+ consumer: 'infrastructure',
+ onClose: onCloseFlyout,
+ canChangeTrigger: false,
+ alertTypeId: METRIC_ANOMALY_ALERT_TYPE_ID,
+ metadata: {
+ metric,
+ nodeType,
+ },
+ }),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [triggersActionsUI, visible]
+ );
+
+ return <>{visible && AddAlertFlyout}>;
+};
+
+export const PrefilledAnomalyAlertFlyout = ({ onClose }: { onClose(): void }) => {
+ const { inventoryPrefill } = useAlertPrefillContext();
+ const { nodeType, metric } = inventoryPrefill;
+
+ return ;
+};
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx
new file mode 100644
index 0000000000000..ae2c6ed81badb
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.test.tsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { mountWithIntl, nextTick } from '@kbn/test/jest';
+// We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock`
+import { coreMock as mockCoreMock } from 'src/core/public/mocks';
+import React from 'react';
+import { Expression, AlertContextMeta } from './expression';
+import { act } from 'react-dom/test-utils';
+
+jest.mock('../../../containers/source/use_source_via_http', () => ({
+ useSourceViaHttp: () => ({
+ source: { id: 'default' },
+ createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }),
+ }),
+}));
+
+jest.mock('../../../hooks/use_kibana', () => ({
+ useKibanaContextForPlugin: () => ({
+ services: mockCoreMock.createStart(),
+ }),
+}));
+
+jest.mock('../../../containers/ml/infra_ml_capabilities', () => ({
+ useInfraMLCapabilities: () => ({
+ isLoading: false,
+ hasInfraMLCapabilities: true,
+ }),
+}));
+
+describe('Expression', () => {
+ async function setup(currentOptions: AlertContextMeta) {
+ const alertParams = {
+ metric: undefined,
+ nodeType: undefined,
+ threshold: 50,
+ };
+ const wrapper = mountWithIntl(
+ Reflect.set(alertParams, key, value)}
+ setAlertProperty={() => {}}
+ metadata={currentOptions}
+ />
+ );
+
+ const update = async () =>
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
+ await update();
+
+ return { wrapper, update, alertParams };
+ }
+
+ it('should prefill the alert using the context metadata', async () => {
+ const currentOptions = {
+ nodeType: 'pod',
+ metric: { type: 'tx' },
+ };
+ const { alertParams } = await setup(currentOptions as AlertContextMeta);
+ expect(alertParams.nodeType).toBe('k8s');
+ expect(alertParams.metric).toBe('network_out');
+ });
+});
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx
new file mode 100644
index 0000000000000..5938c7119616f
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx
@@ -0,0 +1,320 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { pick } from 'lodash';
+import React, { useCallback, useState, useMemo, useEffect } from 'react';
+import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities';
+import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
+import { AlertPreview } from '../../common';
+import {
+ METRIC_ANOMALY_ALERT_TYPE_ID,
+ MetricAnomalyParams,
+} from '../../../../common/alerting/metrics';
+import { euiStyled, EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
+import {
+ WhenExpression,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../../../triggers_actions_ui/public/common';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
+import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
+import { findInventoryModel } from '../../../../common/inventory_models';
+import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
+import { NodeTypeExpression } from './node_type';
+import { SeverityThresholdExpression } from './severity_threshold';
+import { InfraWaffleMapOptions } from '../../../lib/lib';
+import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
+
+import { validateMetricAnomaly } from './validation';
+import { InfluencerFilter } from './influencer_filter';
+import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
+
+export interface AlertContextMeta {
+ metric?: InfraWaffleMapOptions['metric'];
+ nodeType?: InventoryItemType;
+}
+
+interface Props {
+ errors: IErrorObject[];
+ alertParams: MetricAnomalyParams & {
+ sourceId: string;
+ };
+ alertInterval: string;
+ alertThrottle: string;
+ setAlertParams(key: string, value: any): void;
+ setAlertProperty(key: string, value: any): void;
+ metadata: AlertContextMeta;
+}
+
+export const defaultExpression = {
+ metric: 'memory_usage' as MetricAnomalyParams['metric'],
+ threshold: ANOMALY_THRESHOLD.MAJOR,
+ nodeType: 'hosts',
+ influencerFilter: undefined,
+};
+
+export const Expression: React.FC = (props) => {
+ const { hasInfraMLCapabilities, isLoading: isLoadingMLCapabilities } = useInfraMLCapabilities();
+ const { http, notifications } = useKibanaContextForPlugin().services;
+ const { setAlertParams, alertParams, alertInterval, alertThrottle, metadata } = props;
+ const { source, createDerivedIndexPattern } = useSourceViaHttp({
+ sourceId: 'default',
+ type: 'metrics',
+ fetch: http.fetch,
+ toastWarning: notifications.toasts.addWarning,
+ });
+
+ const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
+ createDerivedIndexPattern,
+ ]);
+
+ const [influencerFieldName, updateInfluencerFieldName] = useState(
+ alertParams.influencerFilter?.fieldName ?? 'host.name'
+ );
+
+ useEffect(() => {
+ setAlertParams('hasInfraMLCapabilities', hasInfraMLCapabilities);
+ }, [setAlertParams, hasInfraMLCapabilities]);
+
+ useEffect(() => {
+ if (alertParams.influencerFilter) {
+ setAlertParams('influencerFilter', {
+ ...alertParams.influencerFilter,
+ fieldName: influencerFieldName,
+ });
+ }
+ }, [influencerFieldName, alertParams, setAlertParams]);
+ const updateInfluencerFieldValue = useCallback(
+ (value: string) => {
+ if (value) {
+ setAlertParams('influencerFilter', {
+ ...alertParams.influencerFilter,
+ fieldValue: value,
+ });
+ } else {
+ setAlertParams('influencerFilter', undefined);
+ }
+ },
+ [setAlertParams, alertParams]
+ );
+
+ useEffect(() => {
+ setAlertParams('alertInterval', alertInterval);
+ }, [setAlertParams, alertInterval]);
+
+ const updateNodeType = useCallback(
+ (nt: any) => {
+ setAlertParams('nodeType', nt);
+ },
+ [setAlertParams]
+ );
+
+ const updateMetric = useCallback(
+ (metric: string) => {
+ setAlertParams('metric', metric);
+ },
+ [setAlertParams]
+ );
+
+ const updateSeverityThreshold = useCallback(
+ (threshold: any) => {
+ setAlertParams('threshold', threshold);
+ },
+ [setAlertParams]
+ );
+
+ const prefillNodeType = useCallback(() => {
+ const md = metadata;
+ if (md && md.nodeType) {
+ setAlertParams(
+ 'nodeType',
+ getMLNodeTypeFromInventoryNodeType(md.nodeType) ?? defaultExpression.nodeType
+ );
+ } else {
+ setAlertParams('nodeType', defaultExpression.nodeType);
+ }
+ }, [metadata, setAlertParams]);
+
+ const prefillMetric = useCallback(() => {
+ const md = metadata;
+ if (md && md.metric) {
+ setAlertParams(
+ 'metric',
+ getMLMetricFromInventoryMetric(md.metric.type) ?? defaultExpression.metric
+ );
+ } else {
+ setAlertParams('metric', defaultExpression.metric);
+ }
+ }, [metadata, setAlertParams]);
+
+ useEffect(() => {
+ if (!alertParams.nodeType) {
+ prefillNodeType();
+ }
+
+ if (!alertParams.threshold) {
+ setAlertParams('threshold', defaultExpression.threshold);
+ }
+
+ if (!alertParams.metric) {
+ prefillMetric();
+ }
+
+ if (!alertParams.sourceId) {
+ setAlertParams('sourceId', source?.id || 'default');
+ }
+ }, [metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ if (isLoadingMLCapabilities) return ;
+ if (!hasInfraMLCapabilities) return ;
+
+ return (
+ // https://github.com/elastic/kibana/issues/89506
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+// required for dynamic import
+// eslint-disable-next-line import/no-default-export
+export default Expression;
+
+const StyledExpressionRow = euiStyled(EuiFlexGroup)`
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0 -4px;
+`;
+
+const StyledExpression = euiStyled.div`
+ padding: 0 4px;
+`;
+
+const getDisplayNameForType = (type: InventoryItemType) => {
+ const inventoryModel = findInventoryModel(type);
+ return inventoryModel.displayName;
+};
+
+export const nodeTypes: { [key: string]: any } = {
+ hosts: {
+ text: getDisplayNameForType('host'),
+ value: 'hosts',
+ },
+ k8s: {
+ text: getDisplayNameForType('pod'),
+ value: 'k8s',
+ },
+};
+
+const getMLMetricFromInventoryMetric = (metric: SnapshotMetricType) => {
+ switch (metric) {
+ case 'memory':
+ return 'memory_usage';
+ case 'tx':
+ return 'network_out';
+ case 'rx':
+ return 'network_in';
+ default:
+ return null;
+ }
+};
+
+const getMLNodeTypeFromInventoryNodeType = (nodeType: InventoryItemType) => {
+ switch (nodeType) {
+ case 'host':
+ return 'hosts';
+ case 'pod':
+ return 'k8s';
+ default:
+ return null;
+ }
+};
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx
new file mode 100644
index 0000000000000..34a917a77dcf5
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/influencer_filter.tsx
@@ -0,0 +1,193 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { debounce } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import React, { useState, useCallback, useEffect, useMemo } from 'react';
+import { first } from 'lodash';
+import { EuiFlexGroup, EuiFormRow, EuiCheckbox, EuiFlexItem, EuiSelect } from '@elastic/eui';
+import {
+ MetricsExplorerKueryBar,
+ CurryLoadSuggestionsType,
+} from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
+import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
+
+interface Props {
+ fieldName: string;
+ fieldValue: string;
+ nodeType: MetricAnomalyParams['nodeType'];
+ onChangeFieldName: (v: string) => void;
+ onChangeFieldValue: (v: string) => void;
+ derivedIndexPattern: Parameters[0]['derivedIndexPattern'];
+}
+
+const FILTER_TYPING_DEBOUNCE_MS = 500;
+
+export const InfluencerFilter = ({
+ fieldName,
+ fieldValue,
+ nodeType,
+ onChangeFieldName,
+ onChangeFieldValue,
+ derivedIndexPattern,
+}: Props) => {
+ const fieldNameOptions = useMemo(() => (nodeType === 'k8s' ? k8sFieldNames : hostFieldNames), [
+ nodeType,
+ ]);
+
+ // If initial props contain a fieldValue, assume it was passed in from loaded alertParams,
+ // and enable the UI element
+ const [isEnabled, updateIsEnabled] = useState(fieldValue ? true : false);
+ const [storedFieldValue, updateStoredFieldValue] = useState(fieldValue);
+
+ useEffect(
+ () =>
+ nodeType === 'k8s'
+ ? onChangeFieldName(first(k8sFieldNames)!.value)
+ : onChangeFieldName(first(hostFieldNames)!.value),
+ [nodeType, onChangeFieldName]
+ );
+
+ const onSelectFieldName = useCallback((e) => onChangeFieldName(e.target.value), [
+ onChangeFieldName,
+ ]);
+ const onUpdateFieldValue = useCallback(
+ (value) => {
+ updateStoredFieldValue(value);
+ onChangeFieldValue(value);
+ },
+ [onChangeFieldValue]
+ );
+
+ const toggleEnabled = useCallback(() => {
+ const nextState = !isEnabled;
+ updateIsEnabled(nextState);
+ if (!nextState) {
+ onChangeFieldValue('');
+ } else {
+ onChangeFieldValue(storedFieldValue);
+ }
+ }, [isEnabled, updateIsEnabled, onChangeFieldValue, storedFieldValue]);
+
+ /* eslint-disable-next-line react-hooks/exhaustive-deps */
+ const debouncedOnUpdateFieldValue = useCallback(
+ debounce(onUpdateFieldValue, FILTER_TYPING_DEBOUNCE_MS),
+ [onUpdateFieldValue]
+ );
+
+ const affixFieldNameToQuery: CurryLoadSuggestionsType = (fn) => (
+ expression,
+ cursorPosition,
+ maxSuggestions
+ ) => {
+ // Add the field name to the front of the passed-in query
+ const prefix = `${fieldName}:`;
+ // Trim whitespace to prevent AND/OR suggestions
+ const modifiedExpression = `${prefix}${expression}`.trim();
+ // Move the cursor position forward by the length of the field name
+ const modifiedPosition = cursorPosition + prefix.length;
+ return fn(modifiedExpression, modifiedPosition, maxSuggestions, (suggestions) =>
+ suggestions
+ .map((s) => ({
+ ...s,
+ // Remove quotes from suggestions
+ text: s.text.replace(/\"/g, '').trim(),
+ // Offset the returned suggestions' cursor positions so that they can be autocompleted accurately
+ start: s.start - prefix.length,
+ end: s.end - prefix.length,
+ }))
+ // Removing quotes can lead to an already-selected suggestion still coming up in the autocomplete list,
+ // so filter these out
+ .filter((s) => !expression.startsWith(s.text))
+ );
+ };
+
+ return (
+
+ }
+ helpText={
+ isEnabled ? (
+ <>
+ {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpText', {
+ defaultMessage:
+ 'Limit the scope of your alert trigger to anomalies influenced by certain node(s).',
+ })}
+
+ {i18n.translate('xpack.infra.metrics.alertFlyout.anomalyFilterHelpTextExample', {
+ defaultMessage: 'For example: "my-node-1" or "my-node-*"',
+ })}
+ >
+ ) : null
+ }
+ fullWidth
+ display="rowCompressed"
+ >
+ {isEnabled ? (
+
+
+
+
+
+
+
+
+ ) : (
+ <>>
+ )}
+
+ );
+};
+
+const hostFieldNames = [
+ {
+ value: 'host.name',
+ text: 'host.name',
+ },
+];
+
+const k8sFieldNames = [
+ {
+ value: 'kubernetes.pod.uid',
+ text: 'kubernetes.pod.uid',
+ },
+ {
+ value: 'kubernetes.node.name',
+ text: 'kubernetes.node.name',
+ },
+ {
+ value: 'kubernetes.namespace',
+ text: 'kubernetes.namespace',
+ },
+];
+
+const filterByNodeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.filterByNodeLabel', {
+ defaultMessage: 'Filter by node',
+});
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx
new file mode 100644
index 0000000000000..6ddcf8fd5cb65
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/node_type.tsx
@@ -0,0 +1,117 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui';
+import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui';
+import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
+
+type Node = MetricAnomalyParams['nodeType'];
+
+interface WhenExpressionProps {
+ value: Node;
+ options: { [key: string]: { text: string; value: Node } };
+ onChange: (value: Node) => void;
+ popupPosition?:
+ | 'upCenter'
+ | 'upLeft'
+ | 'upRight'
+ | 'downCenter'
+ | 'downLeft'
+ | 'downRight'
+ | 'leftCenter'
+ | 'leftUp'
+ | 'leftDown'
+ | 'rightCenter'
+ | 'rightUp'
+ | 'rightDown';
+}
+
+export const NodeTypeExpression = ({
+ value,
+ options,
+ onChange,
+ popupPosition,
+}: WhenExpressionProps) => {
+ const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false);
+
+ return (
+ {
+ setAggTypePopoverOpen(true);
+ }}
+ />
+ }
+ isOpen={aggTypePopoverOpen}
+ closePopover={() => {
+ setAggTypePopoverOpen(false);
+ }}
+ ownFocus
+ anchorPosition={popupPosition ?? 'downLeft'}
+ >
+
+ setAggTypePopoverOpen(false)}>
+
+
+ {
+ onChange(e.target.value as Node);
+ setAggTypePopoverOpen(false);
+ }}
+ options={Object.values(options).map((o) => o)}
+ />
+
+
+ );
+};
+
+interface ClosablePopoverTitleProps {
+ children: JSX.Element;
+ onClose: () => void;
+}
+
+export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => {
+ return (
+
+
+ {children}
+
+ onClose()}
+ />
+
+
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx
new file mode 100644
index 0000000000000..2dc561ff172b9
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/severity_threshold.tsx
@@ -0,0 +1,140 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui';
+import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui';
+import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
+
+interface WhenExpressionProps {
+ value: Exclude;
+ onChange: (value: ANOMALY_THRESHOLD) => void;
+ popupPosition?:
+ | 'upCenter'
+ | 'upLeft'
+ | 'upRight'
+ | 'downCenter'
+ | 'downLeft'
+ | 'downRight'
+ | 'leftCenter'
+ | 'leftUp'
+ | 'leftDown'
+ | 'rightCenter'
+ | 'rightUp'
+ | 'rightDown';
+}
+
+const options = {
+ [ANOMALY_THRESHOLD.CRITICAL]: {
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.criticalLabel', {
+ defaultMessage: 'Critical',
+ }),
+ value: ANOMALY_THRESHOLD.CRITICAL,
+ },
+ [ANOMALY_THRESHOLD.MAJOR]: {
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.majorLabel', {
+ defaultMessage: 'Major',
+ }),
+ value: ANOMALY_THRESHOLD.MAJOR,
+ },
+ [ANOMALY_THRESHOLD.MINOR]: {
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.minorLabel', {
+ defaultMessage: 'Minor',
+ }),
+ value: ANOMALY_THRESHOLD.MINOR,
+ },
+ [ANOMALY_THRESHOLD.WARNING]: {
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.severityScore.warningLabel', {
+ defaultMessage: 'Warning',
+ }),
+ value: ANOMALY_THRESHOLD.WARNING,
+ },
+};
+
+export const SeverityThresholdExpression = ({
+ value,
+ onChange,
+ popupPosition,
+}: WhenExpressionProps) => {
+ const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false);
+
+ return (
+ {
+ setAggTypePopoverOpen(true);
+ }}
+ />
+ }
+ isOpen={aggTypePopoverOpen}
+ closePopover={() => {
+ setAggTypePopoverOpen(false);
+ }}
+ ownFocus
+ anchorPosition={popupPosition ?? 'downLeft'}
+ >
+
+ setAggTypePopoverOpen(false)}>
+
+
+ {
+ onChange(Number(e.target.value) as ANOMALY_THRESHOLD);
+ setAggTypePopoverOpen(false);
+ }}
+ options={Object.values(options).map((o) => o)}
+ />
+
+
+ );
+};
+
+interface ClosablePopoverTitleProps {
+ children: JSX.Element;
+ onClose: () => void;
+}
+
+export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => {
+ return (
+
+
+ {children}
+
+ onClose()}
+ />
+
+
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx
new file mode 100644
index 0000000000000..8e254fb2b67a8
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/validation.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { ValidationResult } from '../../../../../triggers_actions_ui/public/types';
+
+export function validateMetricAnomaly({
+ hasInfraMLCapabilities,
+}: {
+ hasInfraMLCapabilities: boolean;
+}): ValidationResult {
+ const validationResult = { errors: {} };
+ const errors: {
+ hasInfraMLCapabilities: string[];
+ } = {
+ hasInfraMLCapabilities: [],
+ };
+
+ validationResult.errors = errors;
+
+ if (!hasInfraMLCapabilities) {
+ errors.hasInfraMLCapabilities.push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.mlCapabilitiesRequired', {
+ defaultMessage: 'Cannot create an anomaly alert when machine learning is disabled.',
+ })
+ );
+ }
+
+ return validationResult;
+}
diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts
new file mode 100644
index 0000000000000..31fed514bdacc
--- /dev/null
+++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../common/alerting/metrics';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
+import { AlertTypeParams } from '../../../../alerts/common';
+import { validateMetricAnomaly } from './components/validation';
+
+interface MetricAnomalyAlertTypeParams extends AlertTypeParams {
+ hasInfraMLCapabilities: boolean;
+}
+
+export function createMetricAnomalyAlertType(): AlertTypeModel {
+ return {
+ id: METRIC_ANOMALY_ALERT_TYPE_ID,
+ description: i18n.translate('xpack.infra.metrics.anomaly.alertFlyout.alertDescription', {
+ defaultMessage: 'Alert when the anomaly score exceeds a defined threshold.',
+ }),
+ iconClass: 'bell',
+ documentationUrl(docLinks) {
+ return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/metric-anomaly-alert.html`;
+ },
+ alertParamsExpression: React.lazy(() => import('./components/expression')),
+ validate: validateMetricAnomaly,
+ defaultActionMessage: i18n.translate(
+ 'xpack.infra.metrics.alerting.anomaly.defaultActionMessage',
+ {
+ defaultMessage: `\\{\\{alertName\\}\\} is in a state of \\{\\{context.alertState\\}\\}
+
+\\{\\{context.metric\\}\\} was \\{\\{context.summary\\}\\} than normal at \\{\\{context.timestamp\\}\\}
+
+Typical value: \\{\\{context.typical\\}\\}
+Actual value: \\{\\{context.actual\\}\\}
+`,
+ }
+ ),
+ requiresAppContext: false,
+ };
+}
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx
deleted file mode 100644
index 3bbe811225825..0000000000000
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx
+++ /dev/null
@@ -1,57 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useState, useCallback } from 'react';
-import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { useAlertPrefillContext } from '../../use_alert_prefill';
-import { AlertFlyout } from './alert_flyout';
-import { ManageAlertsContextMenuItem } from '../../inventory/components/manage_alerts_context_menu_item';
-
-export const MetricsAlertDropdown = () => {
- const [popoverOpen, setPopoverOpen] = useState(false);
- const [flyoutVisible, setFlyoutVisible] = useState(false);
-
- const { metricThresholdPrefill } = useAlertPrefillContext();
- const { groupBy, filterQuery, metrics } = metricThresholdPrefill;
-
- const closePopover = useCallback(() => {
- setPopoverOpen(false);
- }, [setPopoverOpen]);
-
- const openPopover = useCallback(() => {
- setPopoverOpen(true);
- }, [setPopoverOpen]);
-
- const menuItems = [
- setFlyoutVisible(true)}>
-
- ,
- ,
- ];
-
- return (
- <>
-
-
-
- }
- isOpen={popoverOpen}
- closePopover={closePopover}
- >
-
-
-
- >
- );
-};
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
index 929654ecb4693..e7e4ade5257fc 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx
@@ -7,10 +7,10 @@
import React, { useCallback, useContext, useMemo } from 'react';
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types';
+import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
+import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill';
interface Props {
visible?: boolean;
@@ -42,3 +42,10 @@ export const AlertFlyout = (props: Props) => {
return <>{visible && AddAlertFlyout}>;
};
+
+export const PrefilledThresholdAlertFlyout = ({ onClose }: { onClose(): void }) => {
+ const { metricThresholdPrefill } = useAlertPrefillContext();
+ const { groupBy, filterQuery, metrics } = metricThresholdPrefill;
+
+ return ;
+};
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
index 939834fa7c4a8..7e4209e4253d7 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx
@@ -64,6 +64,7 @@ describe('ExpressionChart', () => {
pod: 'kubernetes.pod.uid',
tiebreaker: '_doc',
},
+ anomalyThreshold: 20,
},
};
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
index 65842089863f3..c98984b5475cd 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx
@@ -111,7 +111,9 @@ export const ExpressionChart: React.FC = ({
);
}
- const thresholds = expression.threshold.slice().sort();
+ const criticalThresholds = expression.threshold.slice().sort();
+ const warningThresholds = expression.warningThreshold?.slice().sort() ?? [];
+ const thresholds = [...criticalThresholds, ...warningThresholds].sort();
// Creating a custom series where the ID is changed to 0
// so that we can get a proper domian
@@ -145,108 +147,70 @@ export const ExpressionChart: React.FC = ({
const dataDomain = calculateDomain(series, [metric], false);
const domain = {
max: Math.max(dataDomain.max, last(thresholds) || dataDomain.max) * 1.1, // add 10% headroom.
- min: Math.min(dataDomain.min, first(thresholds) || dataDomain.min),
+ min: Math.min(dataDomain.min, first(thresholds) || dataDomain.min) * 0.9, // add 10% floor,
};
if (domain.min === first(expression.threshold)) {
domain.min = domain.min * 0.9;
}
- const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(expression.comparator);
- const isBelow = [Comparator.LT, Comparator.LT_OR_EQ].includes(expression.comparator);
const opacity = 0.3;
const { timeSize, timeUnit } = expression;
const timeLabel = TIME_LABELS[timeUnit as keyof typeof TIME_LABELS];
- return (
- <>
-
-
-
- ({
- dataValue: threshold,
- }))}
- style={{
- line: {
- strokeWidth: 2,
- stroke: colorTransformer(Color.color1),
- opacity: 1,
- },
- }}
- />
- {thresholds.length === 2 && expression.comparator === Comparator.BETWEEN ? (
- <>
-
- >
- ) : null}
- {thresholds.length === 2 && expression.comparator === Comparator.OUTSIDE_RANGE ? (
- <>
-
- & { sortedThresholds: number[]; color: Color; id: string }) => {
+ if (!comparator || !threshold) return null;
+ const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(comparator);
+ const isBelow = [Comparator.LT, Comparator.LT_OR_EQ].includes(comparator);
+ return (
+ <>
+ ({
+ dataValue: t,
+ }))}
+ style={{
+ line: {
+ strokeWidth: 2,
+ stroke: colorTransformer(color),
+ opacity: 1,
+ },
+ }}
+ />
+ {sortedThresholds.length === 2 && comparator === Comparator.BETWEEN ? (
+ <>
+
- >
- ) : null}
- {isBelow && first(expression.threshold) != null ? (
+ },
+ ]}
+ />
+ >
+ ) : null}
+ {sortedThresholds.length === 2 && comparator === Comparator.OUTSIDE_RANGE ? (
+ <>
= ({
x0: firstTimestamp,
x1: lastTimestamp,
y0: domain.min,
- y1: first(expression.threshold),
+ y1: first(threshold),
},
},
]}
/>
- ) : null}
- {isAbove && first(expression.threshold) != null ? (
= ({
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
- y0: first(expression.threshold),
+ y0: last(threshold),
y1: domain.max,
},
},
]}
/>
- ) : null}
+ >
+ ) : null}
+ {isBelow && first(threshold) != null ? (
+
+ ) : null}
+ {isAbove && first(threshold) != null ? (
+
+ ) : null}
+ >
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+ {expression.warningComparator && expression.warningThreshold && (
+
+ )}
= (props) => {
const [isExpanded, setRowState] = useState(true);
const toggleRowState = useCallback(() => setRowState(!isExpanded), [isExpanded]);
@@ -85,9 +92,14 @@ export const ExpressionRow: React.FC = (props) => {
metric,
comparator = Comparator.GT,
threshold = [],
+ warningThreshold = [],
+ warningComparator,
} = expression;
+ const [displayWarningThreshold, setDisplayWarningThreshold] = useState(
+ Boolean(warningThreshold?.length)
+ );
- const isMetricPct = useMemo(() => metric && metric.endsWith('.pct'), [metric]);
+ const isMetricPct = useMemo(() => Boolean(metric && metric.endsWith('.pct')), [metric]);
const updateAggType = useCallback(
(at: string) => {
@@ -114,22 +126,81 @@ export const ExpressionRow: React.FC = (props) => {
[expressionId, expression, setAlertParams]
);
+ const updateWarningComparator = useCallback(
+ (c?: string) => {
+ setAlertParams(expressionId, { ...expression, warningComparator: c as Comparator });
+ },
+ [expressionId, expression, setAlertParams]
+ );
+
+ const convertThreshold = useCallback(
+ (enteredThreshold) =>
+ isMetricPct ? enteredThreshold.map((v: number) => pctToDecimal(v)) : enteredThreshold,
+ [isMetricPct]
+ );
+
const updateThreshold = useCallback(
(enteredThreshold) => {
- const t = isMetricPct
- ? enteredThreshold.map((v: number) => pctToDecimal(v))
- : enteredThreshold;
+ const t = convertThreshold(enteredThreshold);
if (t.join() !== expression.threshold.join()) {
setAlertParams(expressionId, { ...expression, threshold: t });
}
},
- [expressionId, expression, isMetricPct, setAlertParams]
+ [expressionId, expression, convertThreshold, setAlertParams]
);
- const displayedThreshold = useMemo(() => {
- if (isMetricPct) return threshold.map((v) => decimalToPct(v));
- return threshold;
- }, [threshold, isMetricPct]);
+ const updateWarningThreshold = useCallback(
+ (enteredThreshold) => {
+ const t = convertThreshold(enteredThreshold);
+ if (t.join() !== expression.warningThreshold?.join()) {
+ setAlertParams(expressionId, { ...expression, warningThreshold: t });
+ }
+ },
+ [expressionId, expression, convertThreshold, setAlertParams]
+ );
+
+ const toggleWarningThreshold = useCallback(() => {
+ if (!displayWarningThreshold) {
+ setDisplayWarningThreshold(true);
+ setAlertParams(expressionId, {
+ ...expression,
+ warningComparator: comparator,
+ warningThreshold: [],
+ });
+ } else {
+ setDisplayWarningThreshold(false);
+ setAlertParams(expressionId, omit(expression, 'warningComparator', 'warningThreshold'));
+ }
+ }, [
+ displayWarningThreshold,
+ setDisplayWarningThreshold,
+ setAlertParams,
+ comparator,
+ expression,
+ expressionId,
+ ]);
+
+ const criticalThresholdExpression = (
+
+ );
+
+ const warningThresholdExpression = displayWarningThreshold && (
+
+ );
return (
<>
@@ -187,26 +258,62 @@ export const ExpressionRow: React.FC = (props) => {
/>
)}
-
-
-
- {isMetricPct && (
-
- %
-
- )}
+ {!displayWarningThreshold && criticalThresholdExpression}
+ {displayWarningThreshold && (
+ <>
+
+ {criticalThresholdExpression}
+
+
+
+
+
+ {warningThresholdExpression}
+
+
+
+
+
+ >
+ )}
+ {!displayWarningThreshold && (
+ <>
+ {' '}
+
+
+
+
+
+
+ >
+ )}
{canDelete && (
@@ -227,6 +334,44 @@ export const ExpressionRow: React.FC = (props) => {
);
};
+const ThresholdElement: React.FC<{
+ updateComparator: (c?: string) => void;
+ updateThreshold: (t?: number[]) => void;
+ threshold: MetricExpression['threshold'];
+ isMetricPct: boolean;
+ comparator: MetricExpression['comparator'];
+ errors: IErrorObject;
+}> = ({ updateComparator, updateThreshold, threshold, isMetricPct, comparator, errors }) => {
+ const displayedThreshold = useMemo(() => {
+ if (isMetricPct) return threshold.map((v) => decimalToPct(v));
+ return threshold;
+ }, [threshold, isMetricPct]);
+
+ return (
+ <>
+
+
+
+ {isMetricPct && (
+
+ %
+
+ )}
+ >
+ );
+};
+
export const aggregationType: { [key: string]: any } = {
avg: {
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', {
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx
index bab396df9da0d..69b2f1d1bcc8f 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx
@@ -25,8 +25,14 @@ export function validateMetricThreshold({
aggField: string[];
timeSizeUnit: string[];
timeWindowSize: string[];
- threshold0: string[];
- threshold1: string[];
+ critical: {
+ threshold0: string[];
+ threshold1: string[];
+ };
+ warning: {
+ threshold0: string[];
+ threshold1: string[];
+ };
metric: string[];
};
} = {};
@@ -44,8 +50,14 @@ export function validateMetricThreshold({
aggField: [],
timeSizeUnit: [],
timeWindowSize: [],
- threshold0: [],
- threshold1: [],
+ critical: {
+ threshold0: [],
+ threshold1: [],
+ },
+ warning: {
+ threshold0: [],
+ threshold1: [],
+ },
metric: [],
};
if (!c.aggType) {
@@ -57,36 +69,54 @@ export function validateMetricThreshold({
}
if (!c.threshold || !c.threshold.length) {
- errors[id].threshold0.push(
+ errors[id].critical.threshold0.push(
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
defaultMessage: 'Threshold is required.',
})
);
}
- // The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i].
- // We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element.
- if (c.threshold && c.threshold.length && ![...c.threshold].every(isNumber)) {
- [...c.threshold].forEach((v, i) => {
- if (!isNumber(v)) {
- const key = i === 0 ? 'threshold0' : 'threshold1';
- errors[id][key].push(
- i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', {
- defaultMessage: 'Thresholds must contain a valid number.',
- })
- );
- }
- });
- }
-
- if (c.comparator === Comparator.BETWEEN && (!c.threshold || c.threshold.length < 2)) {
- errors[id].threshold1.push(
+ if (c.warningThreshold && !c.warningThreshold.length) {
+ errors[id].warning.threshold0.push(
i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
defaultMessage: 'Threshold is required.',
})
);
}
+ for (const props of [
+ { comparator: c.comparator, threshold: c.threshold, type: 'critical' },
+ { comparator: c.warningComparator, threshold: c.warningThreshold, type: 'warning' },
+ ]) {
+ // The Threshold component returns an empty array with a length ([empty]) because it's using delete newThreshold[i].
+ // We need to use [...c.threshold] to convert it to an array with an undefined value ([undefined]) so we can test each element.
+ const { comparator, threshold, type } = props as {
+ comparator?: Comparator;
+ threshold?: number[];
+ type: 'critical' | 'warning';
+ };
+ if (threshold && threshold.length && ![...threshold].every(isNumber)) {
+ [...threshold].forEach((v, i) => {
+ if (!isNumber(v)) {
+ const key = i === 0 ? 'threshold0' : 'threshold1';
+ errors[id][type][key].push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired', {
+ defaultMessage: 'Thresholds must contain a valid number.',
+ })
+ );
+ }
+ });
+ }
+
+ if (comparator === Comparator.BETWEEN && (!threshold || threshold.length < 2)) {
+ errors[id][type].threshold1.push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
+ defaultMessage: 'Threshold is required.',
+ })
+ );
+ }
+ }
+
if (!c.timeSize) {
errors[id].timeWindowSize.push(
i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', {
diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx
index 6fcbb0f6ffd4c..20fe816d1dab2 100644
--- a/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx
@@ -7,18 +7,15 @@
import { EuiHealth } from '@elastic/eui';
import React, { useMemo } from 'react';
-import {
- formatAnomalyScore,
- getSeverityCategoryForScore,
- ML_SEVERITY_COLORS,
-} from '../../../../common/log_analysis';
+import { getFormattedSeverityScore } from '../../../../../ml/public';
+import { getSeverityCategoryForScore, ML_SEVERITY_COLORS } from '../../../../common/log_analysis';
export const AnomalySeverityIndicator: React.FunctionComponent<{
anomalyScore: number;
}> = ({ anomalyScore }) => {
const severityColor = useMemo(() => getColorForAnomalyScore(anomalyScore), [anomalyScore]);
- return {formatAnomalyScore(anomalyScore)};
+ return {getFormattedSeverityScore(anomalyScore)};
};
const getColorForAnomalyScore = (anomalyScore: number) => {
diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts
index 1bcc9e7157a51..db5a996c604fc 100644
--- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts
+++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts
@@ -14,4 +14,3 @@ export * from './missing_results_privileges_prompt';
export * from './missing_setup_privileges_prompt';
export * from './ml_unavailable_prompt';
export * from './setup_status_unknown_prompt';
-export * from './subscription_splash_content';
diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx
index ed26bd5b2077c..987ae87423fda 100644
--- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx
@@ -75,7 +75,7 @@ export const ProcessStep: React.FunctionComponent = ({
defaultMessage="Something went wrong creating the necessary ML jobs. Please ensure all selected log indices exist."
/>
- {errorMessages.map((errorMessage, i) => (
+ {setupStatus.reasons.map((errorMessage, i) => (
{errorMessage}
diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx
deleted file mode 100644
index c91c1d82afe9b..0000000000000
--- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx
+++ /dev/null
@@ -1,176 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useEffect } from 'react';
-import { i18n } from '@kbn/i18n';
-import {
- EuiPage,
- EuiPageBody,
- EuiPageContent,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiTitle,
- EuiText,
- EuiButton,
- EuiButtonEmpty,
- EuiImage,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { HttpStart } from 'src/core/public';
-import { LoadingPage } from '../../loading_page';
-
-import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
-import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
-import { useTrialStatus } from '../../../hooks/use_trial_status';
-
-export const SubscriptionSplashContent: React.FC = () => {
- const { services } = useKibana<{ http: HttpStart }>();
- const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus();
-
- useEffect(() => {
- checkTrialAvailability();
- }, [checkTrialAvailability]);
-
- if (loadState === 'pending') {
- return (
-
- );
- }
-
- const canStartTrial = isTrialAvailable && loadState === 'resolved';
-
- let title;
- let description;
- let cta;
-
- if (canStartTrial) {
- title = (
-
- );
-
- description = (
-
- );
-
- cta = (
-
-
-
- );
- } else {
- title = (
-
- );
-
- description = (
-
- );
-
- cta = (
-
-
-
- );
- }
-
- return (
-
-
-
-
-
-
- {title}
-
-
-
- {description}
-
-
- {cta}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-const SubscriptionPage = euiStyled(EuiPage)`
- height: 100%
-`;
-
-const SubscriptionPageContent = euiStyled(EuiPageContent)`
- max-width: 768px !important;
-`;
-
-const SubscriptionPageFooter = euiStyled.div`
- background: ${(props) => props.theme.eui.euiColorLightestShade};
- margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) =>
- props.theme.eui.paddingSizes.l};
- padding: ${(props) => props.theme.eui.paddingSizes.l};
-`;
diff --git a/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx b/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx
new file mode 100644
index 0000000000000..8afd8cde32ef3
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+export const MissingEmbeddableFactoryCallout: React.FC<{ embeddableType: string }> = ({
+ embeddableType,
+}) => {
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts
index 794048a8b3a3a..b4dede79d11f2 100644
--- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts
+++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts
@@ -7,7 +7,11 @@
import { ReactNode, useCallback, useMemo, useState } from 'react';
-import { createInputFieldProps, validateInputFieldNotEmpty } from './input_fields';
+import {
+ createInputFieldProps,
+ createInputRangeFieldProps,
+ validateInputFieldNotEmpty,
+} from './input_fields';
interface FormState {
name: string;
@@ -20,6 +24,7 @@ interface FormState {
podField: string;
tiebreakerField: string;
timestampField: string;
+ anomalyThreshold: number;
}
type FormStateChanges = Partial;
@@ -124,6 +129,17 @@ export const useIndicesConfigurationFormState = ({
}),
[formState.timestampField]
);
+ const anomalyThresholdFieldProps = useMemo(
+ () =>
+ createInputRangeFieldProps({
+ errors: validateInputFieldNotEmpty(formState.anomalyThreshold),
+ name: 'anomalyThreshold',
+ onChange: (anomalyThreshold) =>
+ setFormStateChanges((changes) => ({ ...changes, anomalyThreshold })),
+ value: formState.anomalyThreshold,
+ }),
+ [formState.anomalyThreshold]
+ );
const fieldProps = useMemo(
() => ({
@@ -135,6 +151,7 @@ export const useIndicesConfigurationFormState = ({
podField: podFieldFieldProps,
tiebreakerField: tiebreakerFieldFieldProps,
timestampField: timestampFieldFieldProps,
+ anomalyThreshold: anomalyThresholdFieldProps,
}),
[
nameFieldProps,
@@ -145,6 +162,7 @@ export const useIndicesConfigurationFormState = ({
podFieldFieldProps,
tiebreakerFieldFieldProps,
timestampFieldFieldProps,
+ anomalyThresholdFieldProps,
]
);
@@ -183,4 +201,5 @@ const defaultFormState: FormState = {
podField: '',
tiebreakerField: '',
timestampField: '',
+ anomalyThreshold: 0,
};
diff --git a/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx b/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx
index b8832d27a0a4d..a7a842417ebc2 100644
--- a/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx
+++ b/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { ReactText } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -43,7 +43,47 @@ export const createInputFieldProps = <
value,
});
-export const validateInputFieldNotEmpty = (value: string) =>
+export interface InputRangeFieldProps<
+ Value extends ReactText = ReactText,
+ FieldElement extends HTMLInputElement = HTMLInputElement,
+ ButtonElement extends HTMLButtonElement = HTMLButtonElement
+> {
+ error: React.ReactNode[];
+ isInvalid: boolean;
+ name: string;
+ onChange?: (
+ evt: React.ChangeEvent | React.MouseEvent,
+ isValid: boolean
+ ) => void;
+ value: Value;
+}
+
+export const createInputRangeFieldProps = <
+ Value extends ReactText = ReactText,
+ FieldElement extends HTMLInputElement = HTMLInputElement,
+ ButtonElement extends HTMLButtonElement = HTMLButtonElement
+>({
+ errors,
+ name,
+ onChange,
+ value,
+}: {
+ errors: FieldErrorMessage[];
+ name: string;
+ onChange: (newValue: number, isValid: boolean) => void;
+ value: Value;
+}): InputRangeFieldProps => ({
+ error: errors,
+ isInvalid: errors.length > 0,
+ name,
+ onChange: (
+ evt: React.ChangeEvent | React.MouseEvent,
+ isValid: boolean
+ ) => onChange(+evt.currentTarget.value, isValid),
+ value,
+});
+
+export const validateInputFieldNotEmpty = (value: React.ReactText) =>
value === ''
? [
{
+ return (
+
+
+
+
+
+
+
+
+
+
+ }
+ description={
+
+ }
+ >
+
+ }
+ >
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx
index 3f947bdb40677..c80235137eea6 100644
--- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx
+++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx
@@ -27,6 +27,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi
podField: configuration.fields.pod,
tiebreakerField: configuration.fields.tiebreaker,
timestampField: configuration.fields.timestamp,
+ anomalyThreshold: configuration.anomalyThreshold,
}
: undefined,
[configuration]
@@ -79,6 +80,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi
timestamp: indicesConfigurationFormState.formState.timestampField,
},
logColumns: logColumnsConfigurationFormState.formState.logColumns,
+ anomalyThreshold: indicesConfigurationFormState.formState.anomalyThreshold,
}),
[indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState]
);
@@ -97,6 +99,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi
timestamp: indicesConfigurationFormState.formStateChanges.timestampField,
},
logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns,
+ anomalyThreshold: indicesConfigurationFormState.formStateChanges.anomalyThreshold,
}),
[
indicesConfigurationFormState.formStateChanges,
diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx
index bdf4584bc6287..e63f43470497d 100644
--- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx
+++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx
@@ -26,6 +26,8 @@ import { NameConfigurationPanel } from './name_configuration_panel';
import { useSourceConfigurationFormState } from './source_configuration_form_state';
import { SourceLoadingPage } from '../source_loading_page';
import { Prompt } from '../../utils/navigation_warning_prompt';
+import { MLConfigurationPanel } from './ml_configuration_panel';
+import { useInfraMLCapabilitiesContext } from '../../containers/ml/infra_ml_capabilities';
interface SourceConfigurationSettingsProps {
shouldAllowEdit: boolean;
@@ -52,7 +54,6 @@ export const SourceConfigurationSettings = ({
formState,
formStateChanges,
} = useSourceConfigurationFormState(source && source.configuration);
-
const persistUpdates = useCallback(async () => {
if (sourceExists) {
await updateSourceConfiguration(formStateChanges);
@@ -74,6 +75,8 @@ export const SourceConfigurationSettings = ({
source,
]);
+ const { hasInfraMLCapabilities } = useInfraMLCapabilitiesContext();
+
if ((isLoading || isUninitialized) && !source) {
return ;
}
@@ -125,6 +128,18 @@ export const SourceConfigurationSettings = ({
/>
+ {hasInfraMLCapabilities && (
+ <>
+
+
+
+
+ >
+ )}
{errors.length > 0 ? (
<>
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/subscription_splash_content.tsx
similarity index 58%
rename from x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx
rename to x-pack/plugins/infra/public/components/subscription_splash_content.tsx
index e05759ab57dd5..a6477dfc7d172 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx
+++ b/x-pack/plugins/infra/public/components/subscription_splash_content.tsx
@@ -22,11 +22,11 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { LoadingPage } from '../../../../../../components/loading_page';
-import { useTrialStatus } from '../../../../../../hooks/use_trial_status';
-import { useKibana } from '../../../../../../../../../../src/plugins/kibana_react/public';
-import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
-import { HttpStart } from '../../../../../../../../../../src/core/public';
+import { useKibana } from '../../../../../src/plugins/kibana_react/public';
+import { euiStyled, EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
+import { HttpStart } from '../../../../../src/core/public';
+import { useTrialStatus } from '../hooks/use_trial_status';
+import { LoadingPage } from '../components/loading_page';
export const SubscriptionSplashContent: React.FC = () => {
const { services } = useKibana<{ http: HttpStart }>();
@@ -102,58 +102,60 @@ export const SubscriptionSplashContent: React.FC = () => {
}
return (
-
-
-
-
-
-
- {title}
+
+
+
+
+
+
+
+ {title}
+
+
+
+ {description}
+
+
+ {cta}
+
+
+
+
+
+
+
+
+
+
-
-
- {description}
-
-
- {cta}
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts
index 69846e1f51482..ea1567d6056f1 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts
@@ -91,8 +91,19 @@ const setupMlModuleRequestPayloadRT = rt.intersection([
setupMlModuleRequestParamsRT,
]);
+const setupErrorRT = rt.type({
+ reason: rt.string,
+ type: rt.string,
+});
+
const setupErrorResponseRT = rt.type({
- msg: rt.string,
+ status: rt.number,
+ error: rt.intersection([
+ setupErrorRT,
+ rt.type({
+ root_cause: rt.array(setupErrorRT),
+ }),
+ ]),
});
const datafeedSetupResponseRT = rt.intersection([
diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx
index 72b74d5f99719..00a6c3c2a72fb 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx
+++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx
@@ -11,6 +11,7 @@ import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { useModuleStatus } from './log_analysis_module_status';
import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types';
+import { useUiTracker } from '../../../../../observability/public';
export const useLogAnalysisModule = ({
sourceConfiguration,
@@ -23,6 +24,8 @@ export const useLogAnalysisModule = ({
const { spaceId, sourceId, timestampField } = sourceConfiguration;
const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes);
+ const trackMetric = useUiTracker({ app: 'infra_logs' });
+
const [, fetchJobStatus] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
@@ -75,6 +78,25 @@ export const useLogAnalysisModule = ({
return { setupResult, jobSummaries };
},
onResolve: ({ setupResult: { datafeeds, jobs }, jobSummaries }) => {
+ // Track failures
+ if (
+ [...datafeeds, ...jobs]
+ .reduce((acc, resource) => [...acc, ...Object.keys(resource)], [])
+ .some((key) => key === 'error')
+ ) {
+ const reasons = [...datafeeds, ...jobs]
+ .filter((resource) => resource.error !== undefined)
+ .map((resource) => resource.error?.error?.reason ?? '');
+ // NOTE: Lack of indices and a missing field mapping have the same error
+ if (
+ reasons.filter((reason) => reason.includes('because it has no mappings')).length > 0
+ ) {
+ trackMetric({ metric: 'logs_ml_setup_error_bad_indices_or_mappings' });
+ } else {
+ trackMetric({ metric: 'logs_ml_setup_error_unknown_cause' });
+ }
+ }
+
dispatchModuleStatus({
type: 'finishedSetup',
datafeedSetupResults: datafeeds,
@@ -84,8 +106,11 @@ export const useLogAnalysisModule = ({
sourceId,
});
},
- onReject: () => {
+ onReject: (e: any) => {
dispatchModuleStatus({ type: 'failedSetup' });
+ if (e?.body?.statusCode === 403) {
+ trackMetric({ metric: 'logs_ml_setup_error_lack_of_privileges' });
+ }
},
},
[moduleDescriptor.setUpModule, spaceId, sourceId, timestampField]
diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx
index 1fec67228aa22..c3117c9326d1e 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx
+++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx
@@ -105,10 +105,10 @@ const createStatusReducer = (jobTypes: JobType[]) => (
reasons: [
...Object.values(datafeedSetupResults)
.filter(hasError)
- .map((datafeed) => datafeed.error.msg),
+ .map((datafeed) => datafeed.error.error?.reason),
...Object.values(jobSetupResults)
.filter(hasError)
- .map((job) => job.error.msg),
+ .map((job) => job.error.error?.reason),
],
};
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx
index 72dc4da01d867..661ce8f8a253c 100644
--- a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx
@@ -52,11 +52,11 @@ export const useInfraMLCapabilities = () => {
const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob;
const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs;
- const hasInfraMLCapabilites =
+ const hasInfraMLCapabilities =
mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace;
return {
- hasInfraMLCapabilites,
+ hasInfraMLCapabilities,
hasInfraMLReadCapabilities,
hasInfraMLSetupCapabilities,
isLoading,
diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx
index 379ac9774c242..1a759950f640d 100644
--- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx
+++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx
@@ -56,7 +56,8 @@ class WithKueryAutocompletionComponent extends React.Component<
private loadSuggestions = async (
expression: string,
cursorPosition: number,
- maxSuggestions?: number
+ maxSuggestions?: number,
+ transformSuggestions?: (s: QuerySuggestion[]) => QuerySuggestion[]
) => {
const { indexPattern } = this.props;
const language = 'kuery';
@@ -86,6 +87,10 @@ class WithKueryAutocompletionComponent extends React.Component<
boolFilter: [],
})) || [];
+ const transformedSuggestions = transformSuggestions
+ ? transformSuggestions(suggestions)
+ : suggestions;
+
this.setState((state) =>
state.currentRequest &&
state.currentRequest.expression !== expression &&
@@ -94,7 +99,9 @@ class WithKueryAutocompletionComponent extends React.Component<
: {
...state,
currentRequest: null,
- suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions,
+ suggestions: maxSuggestions
+ ? transformedSuggestions.slice(0, maxSuggestions)
+ : transformedSuggestions,
}
);
};
diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx
index 12b0cb06d8682..15eb525dca734 100644
--- a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx
+++ b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx
@@ -5,11 +5,10 @@
* 2.0.
*/
-import { useCallback } from 'react';
+import { useCallback, useEffect } from 'react';
import useUpdateEffect from 'react-use/lib/useUpdateEffect';
import useMount from 'react-use/lib/useMount';
-
import { useKibanaContextForPlugin } from './use_kibana';
import { TimeRange, TimefilterContract } from '../../../../../src/plugins/data/public';
@@ -29,8 +28,24 @@ export const useKibanaTimefilterTime = ({
return [getTime, services.data.query.timefilter.timefilter.setTime];
};
-export const useSyncKibanaTimeFilterTime = (defaults: TimeRange, currentTimeRange: TimeRange) => {
- const [, setTime] = useKibanaTimefilterTime(defaults);
+/**
+ * Handles one or two way syncing with the Kibana time filter service.
+ *
+ * For one way syncing the time range will be synced back to the time filter service
+ * on mount *if* it differs from the defaults, e.g. a URL param.
+ * Future updates, after mount, will also be synced back to the time filter service.
+ *
+ * For two way syncing, in addition to the above, changes *from* the time filter service
+ * will be sycned to the solution, e.g. there might be an embeddable on the page that
+ * fires an action that hooks into the time filter service.
+ */
+export const useSyncKibanaTimeFilterTime = (
+ defaults: TimeRange,
+ currentTimeRange: TimeRange,
+ setTimeRange?: (timeRange: TimeRange) => void
+) => {
+ const { services } = useKibanaContextForPlugin();
+ const [getTime, setTime] = useKibanaTimefilterTime(defaults);
// On first mount we only want to sync time with Kibana if the derived currentTimeRange (e.g. from URL params)
// differs from our defaults.
@@ -40,8 +55,22 @@ export const useSyncKibanaTimeFilterTime = (defaults: TimeRange, currentTimeRang
}
});
- // Sync explicit changes *after* mount back to Kibana
+ // Sync explicit changes *after* mount from the solution back to Kibana
useUpdateEffect(() => {
setTime({ from: currentTimeRange.from, to: currentTimeRange.to });
}, [currentTimeRange.from, currentTimeRange.to, setTime]);
+
+ // *Optionally* sync time filter service changes back to the solution.
+ // For example, an embeddable might have a time range action that hooks into
+ // the time filter service.
+ useEffect(() => {
+ const sub = services.data.query.timefilter.timefilter.getTimeUpdate$().subscribe(() => {
+ if (setTimeRange) {
+ const timeRange = getTime();
+ setTimeRange(timeRange);
+ }
+ });
+
+ return () => sub.unsubscribe();
+ }, [getTime, setTimeRange, services.data.query.timefilter.timefilter]);
};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
index f0fdd79bcd93d..628df397998ee 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx
@@ -7,13 +7,13 @@
import { i18n } from '@kbn/i18n';
import React, { useCallback, useEffect } from 'react';
+import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
LogAnalysisSetupStatusUnknownPrompt,
MissingResultsPrivilegesPrompt,
MissingSetupPrivilegesPrompt,
- SubscriptionSplashContent,
} from '../../../components/logging/log_analysis_setup';
import {
LogAnalysisSetupFlyout,
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
index 4d06d23ef93ef..5fd00527b8b70 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx
@@ -7,13 +7,13 @@
import { i18n } from '@kbn/i18n';
import React, { memo, useEffect, useCallback } from 'react';
+import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
LogAnalysisSetupStatusUnknownPrompt,
MissingResultsPrivilegesPrompt,
MissingSetupPrivilegesPrompt,
- SubscriptionSplashContent,
} from '../../../components/logging/log_analysis_setup';
import {
LogAnalysisSetupFlyout,
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
index a8660e1ce8013..54617d025652b 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx
@@ -5,17 +5,14 @@
* 2.0.
*/
-import datemath from '@elastic/datemath';
import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui';
import moment from 'moment';
import { stringify } from 'query-string';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useMemo } from 'react';
import { encode, RisonValue } from 'rison-node';
-import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useTrackPageview } from '../../../../../observability/public';
-import { TimeRange } from '../../../../common/time/time_range';
-import { bucketSpan } from '../../../../common/log_analysis';
import { TimeKey } from '../../../../common/time';
import {
CategoryJobNoticesSection,
@@ -29,14 +26,11 @@ import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogEntryFlyoutContext } from '../../../containers/logs/log_flyout';
import { useLogSourceContext } from '../../../containers/logs/log_source';
-import { useInterval } from '../../../hooks/use_interval';
import { AnomaliesResults } from './sections/anomalies';
import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results';
-import { useLogEntryRateResults } from './use_log_entry_rate_results';
-import {
- StringTimeRange,
- useLogAnalysisResultsUrlState,
-} from './use_log_entry_rate_results_url_state';
+import { useDatasetFiltering } from './use_dataset_filtering';
+import { useLogAnalysisResultsUrlState } from './use_log_entry_rate_results_url_state';
+import { isJobStatusWithResults } from '../../../../common/log_analysis';
export const SORT_DEFAULTS = {
direction: 'desc' as const,
@@ -62,6 +56,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
hasStoppedJobs: hasStoppedLogEntryRateJobs,
moduleDescriptor: logEntryRateModuleDescriptor,
setupStatus: logEntryRateSetupStatus,
+ jobStatus: logEntryRateJobStatus,
+ jobIds: logEntryRateJobIds,
} = useLogEntryRateModuleContext();
const {
@@ -71,10 +67,29 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
hasStoppedJobs: hasStoppedLogEntryCategoriesJobs,
moduleDescriptor: logEntryCategoriesModuleDescriptor,
setupStatus: logEntryCategoriesSetupStatus,
+ jobStatus: logEntryCategoriesJobStatus,
+ jobIds: logEntryCategoriesJobIds,
} = useLogEntryCategoriesModuleContext();
+ const jobIds = useMemo(() => {
+ return [
+ ...(isJobStatusWithResults(logEntryRateJobStatus['log-entry-rate'])
+ ? [logEntryRateJobIds['log-entry-rate']]
+ : []),
+ ...(isJobStatusWithResults(logEntryCategoriesJobStatus['log-entry-categories-count'])
+ ? [logEntryCategoriesJobIds['log-entry-categories-count']]
+ : []),
+ ];
+ }, [
+ logEntryRateJobIds,
+ logEntryCategoriesJobIds,
+ logEntryRateJobStatus,
+ logEntryCategoriesJobStatus,
+ ]);
+
const {
- timeRange: selectedTimeRange,
+ timeRange,
+ friendlyTimeRange,
setTimeRange: setSelectedTimeRange,
autoRefresh,
setAutoRefresh,
@@ -86,21 +101,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
logEntryId: flyoutLogEntryId,
} = useLogEntryFlyoutContext();
- const [queryTimeRange, setQueryTimeRange] = useState<{
- value: TimeRange;
- lastChangedTime: number;
- }>(() => ({
- value: stringToNumericTimeRange(selectedTimeRange),
- lastChangedTime: Date.now(),
- }));
-
const linkToLogStream = useCallback(
(filter: string, id: string, timeKey?: TimeKey) => {
const params = {
logPosition: encode({
- end: moment(queryTimeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
+ end: moment(timeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
position: timeKey as RisonValue,
- start: moment(queryTimeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
+ start: moment(timeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
streamLive: false,
}),
flyoutOptions: encode({
@@ -114,23 +121,10 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
navigateToApp?.('logs', { path: `/stream?${stringify(params)}` });
},
- [queryTimeRange, navigateToApp]
- );
-
- const bucketDuration = useMemo(
- () => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime),
- [queryTimeRange.value.endTime, queryTimeRange.value.startTime]
+ [timeRange, navigateToApp]
);
- const [selectedDatasets, setSelectedDatasets] = useState([]);
-
- const { getLogEntryRate, isLoading, logEntryRate } = useLogEntryRateResults({
- sourceId,
- startTime: queryTimeRange.value.startTime,
- endTime: queryTimeRange.value.endTime,
- bucketDuration,
- filteredDatasets: selectedDatasets,
- });
+ const { selectedDatasets, setSelectedDatasets } = useDatasetFiltering();
const {
isLoadingLogEntryAnomalies,
@@ -146,48 +140,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
isLoadingDatasets,
} = useLogEntryAnomaliesResults({
sourceId,
- startTime: queryTimeRange.value.startTime,
- endTime: queryTimeRange.value.endTime,
+ startTime: timeRange.value.startTime,
+ endTime: timeRange.value.endTime,
defaultSortOptions: SORT_DEFAULTS,
defaultPaginationOptions: PAGINATION_DEFAULTS,
filteredDatasets: selectedDatasets,
});
- const handleQueryTimeRangeChange = useCallback(
- ({ start: startTime, end: endTime }: { start: string; end: string }) => {
- setQueryTimeRange({
- value: stringToNumericTimeRange({ startTime, endTime }),
- lastChangedTime: Date.now(),
- });
- },
- [setQueryTimeRange]
- );
-
- const handleSelectedTimeRangeChange = useCallback(
- (selectedTime: { start: string; end: string; isInvalid: boolean }) => {
- if (selectedTime.isInvalid) {
- return;
- }
- setSelectedTimeRange({
- startTime: selectedTime.start,
- endTime: selectedTime.end,
- });
- handleQueryTimeRangeChange(selectedTime);
- },
- [setSelectedTimeRange, handleQueryTimeRangeChange]
- );
-
- const handleChartTimeRangeChange = useCallback(
- ({ startTime, endTime }: TimeRange) => {
- handleSelectedTimeRangeChange({
- end: new Date(endTime).toISOString(),
- isInvalid: false,
- start: new Date(startTime).toISOString(),
- });
- },
- [handleSelectedTimeRangeChange]
- );
-
const handleAutoRefreshChange = useCallback(
({ isPaused, refreshInterval: interval }: { isPaused: boolean; refreshInterval: number }) => {
setAutoRefresh({
@@ -207,7 +166,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
showModuleSetup,
]);
- const hasLogRateResults = (logEntryRate?.histogramBuckets?.length ?? 0) > 0;
const hasAnomalyResults = logEntryAnomalies.length > 0;
const isFirstUse = useMemo(
@@ -217,22 +175,18 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
logEntryCategoriesSetupStatus.type === 'succeeded' ||
(logEntryRateSetupStatus.type === 'skipped' && !!logEntryRateSetupStatus.newlyCreated) ||
logEntryRateSetupStatus.type === 'succeeded') &&
- !(hasLogRateResults || hasAnomalyResults),
- [hasAnomalyResults, hasLogRateResults, logEntryCategoriesSetupStatus, logEntryRateSetupStatus]
+ !hasAnomalyResults,
+ [hasAnomalyResults, logEntryCategoriesSetupStatus, logEntryRateSetupStatus]
);
- useEffect(() => {
- getLogEntryRate();
- }, [getLogEntryRate, selectedDatasets, queryTimeRange.lastChangedTime]);
-
- useInterval(
- () => {
- handleQueryTimeRangeChange({
- start: selectedTimeRange.startTime,
- end: selectedTimeRange.endTime,
- });
+ const handleSelectedTimeRangeChange = useCallback(
+ (selectedTime: { start: string; end: string; isInvalid: boolean }) => {
+ if (selectedTime.isInvalid) {
+ return;
+ }
+ setSelectedTimeRange(selectedTime);
},
- autoRefresh.isPaused ? null : autoRefresh.interval
+ [setSelectedTimeRange]
);
return (
@@ -251,8 +205,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
{
{
changePaginationOptions={changePaginationOptions}
sortOptions={sortOptions}
paginationOptions={paginationOptions}
+ selectedDatasets={selectedDatasets}
+ jobIds={jobIds}
+ autoRefresh={autoRefresh}
/>
@@ -318,37 +272,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
);
};
-const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({
- startTime: moment(
- datemath.parse(timeRange.startTime, {
- momentInstance: moment,
- })
- ).valueOf(),
- endTime: moment(
- datemath.parse(timeRange.endTime, {
- momentInstance: moment,
- roundUp: true,
- })
- ).valueOf(),
-});
-
-/**
- * This function takes the current time range in ms,
- * works out the bucket interval we'd need to always
- * display 100 data points, and then takes that new
- * value and works out the nearest multiple of
- * 900000 (15 minutes) to it, so that we don't end up with
- * jaggy bucket boundaries between the ML buckets and our
- * aggregation buckets.
- */
-const getBucketDuration = (startTime: number, endTime: number) => {
- const msRange = moment(endTime).diff(moment(startTime));
- const bucketIntervalInMs = msRange / 100;
- const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan);
- const roundedResult = parseInt(Number(result).toFixed(0), 10);
- return roundedResult < bucketSpan ? bucketSpan : roundedResult;
-};
-
// This is needed due to the flex-basis: 100% !important; rule that
// kicks in on small screens via media queries breaking when using direction="column"
export const ResultsContentPage = euiStyled(EuiPage)`
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx
new file mode 100644
index 0000000000000..b0e85a4648d6e
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import moment from 'moment';
+import { AutoRefresh } from '../../use_log_entry_rate_results_url_state';
+import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
+import {
+ ANOMALY_SWIMLANE_EMBEDDABLE_TYPE,
+ AnomalySwimlaneEmbeddableInput,
+} from '../../../../../../../ml/public';
+import { EmbeddableRenderer } from '../../../../../../../../../src/plugins/embeddable/public';
+import { partitionField } from '../../../../../../common/infra_ml';
+import { MissingEmbeddableFactoryCallout } from '../../../../../components/missing_embeddable_factory_callout';
+import { TimeRange } from '../../../../../../common/time/time_range';
+
+interface Props {
+ timeRange: TimeRange;
+ jobIds: string[];
+ selectedDatasets: string[];
+ autoRefresh: AutoRefresh;
+}
+
+// Disable refresh, allow our timerange changes to refresh the embeddable.
+const REFRESH_CONFIG = {
+ pause: true,
+ value: 0,
+};
+
+export const AnomaliesSwimlaneVisualisation: React.FC = (props) => {
+ const { embeddable: embeddablePlugin } = useKibanaContextForPlugin().services;
+ if (!embeddablePlugin) return null;
+ return ;
+};
+
+export const VisualisationContent: React.FC = ({ timeRange, jobIds, selectedDatasets }) => {
+ const { embeddable: embeddablePlugin } = useKibanaContextForPlugin().services;
+ const factory = embeddablePlugin?.getEmbeddableFactory(ANOMALY_SWIMLANE_EMBEDDABLE_TYPE);
+
+ const embeddableInput: AnomalySwimlaneEmbeddableInput = useMemo(() => {
+ return {
+ id: 'LOG_ENTRY_ANOMALIES_EMBEDDABLE_INSTANCE', // NOTE: This is the only embeddable on the anomalies page, a static string will do.
+ jobIds,
+ swimlaneType: 'viewBy',
+ timeRange: {
+ from: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
+ to: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
+ },
+ refreshConfig: REFRESH_CONFIG,
+ viewBy: partitionField,
+ filters: [],
+ query: {
+ language: 'kuery',
+ query: selectedDatasets
+ .map((dataset) => `${partitionField} : ${dataset !== '' ? dataset : '""'}`)
+ .join(' or '), // Ensure unknown (those with an empty "" string) datasets are handled correctly.
+ },
+ };
+ }, [jobIds, timeRange.startTime, timeRange.endTime, selectedDatasets]);
+
+ if (!factory) {
+ return ;
+ }
+
+ return ;
+};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx
deleted file mode 100644
index dd9c2dd707044..0000000000000
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx
+++ /dev/null
@@ -1,181 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EuiEmptyPrompt } from '@elastic/eui';
-import { RectAnnotationDatum, AnnotationId } from '@elastic/charts';
-import {
- Axis,
- BarSeries,
- Chart,
- niceTimeFormatter,
- Settings,
- TooltipValue,
- LIGHT_THEME,
- DARK_THEME,
- RectAnnotation,
- BrushEndListener,
-} from '@elastic/charts';
-import numeral from '@elastic/numeral';
-import { i18n } from '@kbn/i18n';
-import moment from 'moment';
-import React, { useCallback, useMemo } from 'react';
-import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
-
-import { TimeRange } from '../../../../../../common/time/time_range';
-import {
- MLSeverityScoreCategories,
- ML_SEVERITY_COLORS,
-} from '../../../../../../common/log_analysis';
-import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting';
-
-export const AnomaliesChart: React.FunctionComponent<{
- chartId: string;
- setTimeRange: (timeRange: TimeRange) => void;
- timeRange: TimeRange;
- series: Array<{ time: number; value: number }>;
- annotations: Record;
- renderAnnotationTooltip?: (details?: string) => JSX.Element;
- isLoading: boolean;
-}> = ({
- chartId,
- series,
- annotations,
- setTimeRange,
- timeRange,
- renderAnnotationTooltip,
- isLoading,
-}) => {
- const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss.SSS');
- const [isDarkMode] = useKibanaUiSetting('theme:darkMode');
-
- const chartDateFormatter = useMemo(
- () => niceTimeFormatter([timeRange.startTime, timeRange.endTime]),
- [timeRange]
- );
-
- const logEntryRateSpecId = 'averageValues';
-
- const tooltipProps = useMemo(
- () => ({
- headerFormatter: (tooltipData: TooltipValue) => moment(tooltipData.value).format(dateFormat),
- }),
- [dateFormat]
- );
-
- const handleBrushEnd = useCallback(
- ({ x }) => {
- if (!x) {
- return;
- }
- const [startTime, endTime] = x;
- setTimeRange({
- endTime,
- startTime,
- });
- },
- [setTimeRange]
- );
-
- return !isLoading && !series.length ? (
-
- {i18n.translate('xpack.infra.logs.analysis.anomalySectionLogRateChartNoData', {
- defaultMessage: 'There is no log rate data to display.',
- })}
-
- }
- titleSize="m"
- />
- ) : (
-
-
- {series.length ? (
-
-
- numeral(value.toPrecision(3)).format('0[.][00]a')} // https://github.com/adamwdraper/Numeral-js/issues/194
- />
-
- {renderAnnotations(annotations, chartId, renderAnnotationTooltip)}
-
-
- ) : null}
-
-
- );
-};
-
-interface SeverityConfig {
- id: AnnotationId;
- style: {
- fill: string;
- opacity: number;
- };
-}
-
-const severityConfigs: Record = {
- warning: {
- id: `anomalies-warning`,
- style: { fill: ML_SEVERITY_COLORS.warning, opacity: 0.7 },
- },
- minor: {
- id: `anomalies-minor`,
- style: { fill: ML_SEVERITY_COLORS.minor, opacity: 0.7 },
- },
- major: {
- id: `anomalies-major`,
- style: { fill: ML_SEVERITY_COLORS.major, opacity: 0.7 },
- },
- critical: {
- id: `anomalies-critical`,
- style: { fill: ML_SEVERITY_COLORS.critical, opacity: 0.7 },
- },
-};
-
-const renderAnnotations = (
- annotations: Record,
- chartId: string,
- renderAnnotationTooltip?: (details?: string) => JSX.Element
-) => {
- return Object.entries(annotations).map((entry, index) => {
- return (
-
- );
- });
-};
-
-const barSeriesStyle = { rect: { fill: '#D3DAE6', opacity: 0.6 } }; // TODO: Acquire this from "theme" as euiColorLightShade
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
index 75d7c4212bbc3..3bc206e9ad7bb 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx
@@ -14,12 +14,9 @@ import {
EuiLoadingSpinner,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React, { useMemo } from 'react';
-import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
-import { LogEntryRateResults } from '../../use_log_entry_rate_results';
+import React from 'react';
import { TimeRange } from '../../../../../../common/time/time_range';
-import { getAnnotationsForAll, getLogEntryRateCombinedSeries } from '../helpers/data_formatters';
-import { AnomaliesChart } from './chart';
+import { AnomaliesSwimlaneVisualisation } from './anomalies_swimlane_visualisation';
import { AnomaliesTable } from './table';
import { ManageJobsButton } from '../../../../../components/logging/log_analysis_setup/manage_jobs_button';
import {
@@ -33,13 +30,11 @@ import {
SortOptions,
} from '../../use_log_entry_anomalies_results';
import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
+import { AutoRefresh } from '../../use_log_entry_rate_results_url_state';
export const AnomaliesResults: React.FunctionComponent<{
- isLoadingLogRateResults: boolean;
isLoadingAnomaliesResults: boolean;
- logEntryRateResults: LogEntryRateResults | null;
anomalies: LogEntryAnomalies;
- setTimeRange: (timeRange: TimeRange) => void;
timeRange: TimeRange;
onViewModuleList: () => void;
page: Page;
@@ -49,11 +44,11 @@ export const AnomaliesResults: React.FunctionComponent<{
changePaginationOptions: ChangePaginationOptions;
sortOptions: SortOptions;
paginationOptions: PaginationOptions;
+ selectedDatasets: string[];
+ jobIds: string[];
+ autoRefresh: AutoRefresh;
}> = ({
- isLoadingLogRateResults,
isLoadingAnomaliesResults,
- logEntryRateResults,
- setTimeRange,
timeRange,
onViewModuleList,
anomalies,
@@ -64,27 +59,10 @@ export const AnomaliesResults: React.FunctionComponent<{
fetchNextPage,
fetchPreviousPage,
page,
+ selectedDatasets,
+ jobIds,
+ autoRefresh,
}) => {
- const logEntryRateSeries = useMemo(
- () =>
- logEntryRateResults && logEntryRateResults.histogramBuckets
- ? getLogEntryRateCombinedSeries(logEntryRateResults)
- : [],
- [logEntryRateResults]
- );
- const anomalyAnnotations = useMemo(
- () =>
- logEntryRateResults && logEntryRateResults.histogramBuckets
- ? getAnnotationsForAll(logEntryRateResults)
- : {
- warning: [],
- minor: [],
- major: [],
- critical: [],
- },
- [logEntryRateResults]
- );
-
return (
<>
@@ -98,52 +76,44 @@ export const AnomaliesResults: React.FunctionComponent<{
- {(!logEntryRateResults ||
- (logEntryRateResults &&
- logEntryRateResults.histogramBuckets &&
- !logEntryRateResults.histogramBuckets.length)) &&
- (!anomalies || anomalies.length === 0) ? (
- }
- >
-
- {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataTitle', {
- defaultMessage: 'There is no data to display.',
- })}
-
- }
- titleSize="m"
- body={
-
- {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataBody', {
- defaultMessage: 'You may want to adjust your time range.',
- })}
-
- }
+
+
+
-
- ) : (
- <>
-
-
-
-
-
-
+
+
+
+ <>
+ {!anomalies || anomalies.length === 0 ? (
+ }
+ >
+
+ {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataTitle', {
+ defaultMessage: 'There is no data to display.',
+ })}
+
+ }
+ titleSize="m"
+ body={
+
+ {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataBody', {
+ defaultMessage: 'You may want to adjust your time range.',
+ })}
+
+ }
+ />
+
+ ) : (
- >
- )}
+ )}
+ >
>
);
};
@@ -164,52 +134,6 @@ const title = i18n.translate('xpack.infra.logs.analysis.anomaliesSectionTitle',
defaultMessage: 'Anomalies',
});
-interface ParsedAnnotationDetails {
- anomalyScoresByPartition: Array<{ partitionName: string; maximumAnomalyScore: number }>;
-}
-
-const overallAnomalyScoreLabel = i18n.translate(
- 'xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel',
- {
- defaultMessage: 'Max anomaly scores:',
- }
-);
-
-const AnnotationTooltip: React.FunctionComponent<{ details: string }> = ({ details }) => {
- const parsedDetails: ParsedAnnotationDetails = JSON.parse(details);
- return (
-
-
- {overallAnomalyScoreLabel}
-
-
- {parsedDetails.anomalyScoresByPartition.map(({ partitionName, maximumAnomalyScore }) => {
- return (
- -
-
- {`${partitionName}: `}
- {maximumAnomalyScore}
-
-
- );
- })}
-
-
- );
-};
-
-const renderAnnotationTooltip = (details?: string) => {
- // Note: Seems to be necessary to get things typed correctly all the way through to elastic-charts components
- if (!details) {
- return ;
- }
- return ;
-};
-
-const TooltipWrapper = euiStyled('div')`
- white-space: nowrap;
-`;
-
const loadingAriaLabel = i18n.translate(
'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel',
{ defaultMessage: 'Loading anomalies' }
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx
index d80f9d04e72a8..c208c72558362 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx
@@ -22,7 +22,6 @@ import useSet from 'react-use/lib/useSet';
import { TimeRange } from '../../../../../../common/time/time_range';
import {
AnomalyType,
- formatAnomalyScore,
getFriendlyNameForPartitionId,
formatOneDecimalPlace,
isCategoryAnomaly,
@@ -47,7 +46,6 @@ import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay
interface TableItem {
id: string;
dataset: string;
- datasetName: string;
anomalyScore: number;
startTime: number;
typical: number;
@@ -86,7 +84,6 @@ const datasetColumnName = i18n.translate(
export const AnomaliesTable: React.FunctionComponent<{
results: LogEntryAnomalies;
- setTimeRange: (timeRange: TimeRange) => void;
timeRange: TimeRange;
changeSortOptions: ChangeSortOptions;
changePaginationOptions: ChangePaginationOptions;
@@ -99,7 +96,6 @@ export const AnomaliesTable: React.FunctionComponent<{
}> = ({
results,
timeRange,
- setTimeRange,
changeSortOptions,
sortOptions,
changePaginationOptions,
@@ -122,8 +118,7 @@ export const AnomaliesTable: React.FunctionComponent<{
return {
id: anomaly.id,
dataset: anomaly.dataset,
- datasetName: getFriendlyNameForPartitionId(anomaly.dataset),
- anomalyScore: formatAnomalyScore(anomaly.anomalyScore),
+ anomalyScore: anomaly.anomalyScore,
startTime: anomaly.startTime,
type: anomaly.type,
typical: anomaly.typical,
@@ -182,11 +177,12 @@ export const AnomaliesTable: React.FunctionComponent<{
render: (startTime: number) => moment(startTime).format(dateFormat),
},
{
- field: 'datasetName',
+ field: 'dataset',
name: datasetColumnName,
sortable: true,
truncateText: true,
width: '200px',
+ render: (dataset: string) => getFriendlyNameForPartitionId(dataset),
},
{
align: RIGHT_ALIGNMENT,
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx
deleted file mode 100644
index 8041ad1458546..0000000000000
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx
+++ /dev/null
@@ -1,182 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { RectAnnotationDatum } from '@elastic/charts';
-import { i18n } from '@kbn/i18n';
-
-import {
- formatAnomalyScore,
- getFriendlyNameForPartitionId,
- getSeverityCategoryForScore,
- MLSeverityScoreCategories,
-} from '../../../../../../common/log_analysis';
-import { LogEntryRateResults } from '../../use_log_entry_rate_results';
-
-export const getLogEntryRatePartitionedSeries = (results: LogEntryRateResults) => {
- return results.histogramBuckets.reduce>(
- (buckets, bucket) => {
- return [
- ...buckets,
- ...bucket.partitions.map((partition) => ({
- group: getFriendlyNameForPartitionId(partition.partitionId),
- time: bucket.startTime,
- value: partition.averageActualLogEntryRate,
- })),
- ];
- },
- []
- );
-};
-
-export const getLogEntryRateCombinedSeries = (results: LogEntryRateResults) => {
- return results.histogramBuckets.reduce>(
- (buckets, bucket) => {
- return [
- ...buckets,
- {
- time: bucket.startTime,
- value: bucket.partitions.reduce((accumulatedValue, partition) => {
- return accumulatedValue + partition.averageActualLogEntryRate;
- }, 0),
- },
- ];
- },
- []
- );
-};
-
-export const getLogEntryRateSeriesForPartition = (
- results: LogEntryRateResults,
- partitionId: string
-) => {
- return results.partitionBuckets[partitionId].buckets.reduce<
- Array<{ time: number; value: number }>
- >((buckets, bucket) => {
- return [
- ...buckets,
- {
- time: bucket.startTime,
- value: bucket.averageActualLogEntryRate,
- },
- ];
- }, []);
-};
-
-export const getAnnotationsForPartition = (results: LogEntryRateResults, partitionId: string) => {
- return results.partitionBuckets[partitionId].buckets.reduce<
- Record
- >(
- (annotatedBucketsBySeverity, bucket) => {
- const severityCategory = getSeverityCategoryForScore(bucket.maximumAnomalyScore);
- if (!severityCategory) {
- return annotatedBucketsBySeverity;
- }
-
- return {
- ...annotatedBucketsBySeverity,
- [severityCategory]: [
- ...annotatedBucketsBySeverity[severityCategory],
- {
- coordinates: {
- x0: bucket.startTime,
- x1: bucket.startTime + results.bucketDuration,
- },
- details: i18n.translate(
- 'xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel',
- {
- defaultMessage: 'Max anomaly score: {maxAnomalyScore}',
- values: {
- maxAnomalyScore: formatAnomalyScore(bucket.maximumAnomalyScore),
- },
- }
- ),
- },
- ],
- };
- },
- {
- warning: [],
- minor: [],
- major: [],
- critical: [],
- }
- );
-};
-
-export const getTotalNumberOfLogEntriesForPartition = (
- results: LogEntryRateResults,
- partitionId: string
-) => {
- return results.partitionBuckets[partitionId].totalNumberOfLogEntries;
-};
-
-export const getAnnotationsForAll = (results: LogEntryRateResults) => {
- return results.histogramBuckets.reduce>(
- (annotatedBucketsBySeverity, bucket) => {
- const maxAnomalyScoresByPartition = bucket.partitions.reduce<
- Array<{ partitionName: string; maximumAnomalyScore: number }>
- >((bucketMaxAnomalyScoresByPartition, partition) => {
- if (!getSeverityCategoryForScore(partition.maximumAnomalyScore)) {
- return bucketMaxAnomalyScoresByPartition;
- }
- return [
- ...bucketMaxAnomalyScoresByPartition,
- {
- partitionName: getFriendlyNameForPartitionId(partition.partitionId),
- maximumAnomalyScore: formatAnomalyScore(partition.maximumAnomalyScore),
- },
- ];
- }, []);
-
- if (maxAnomalyScoresByPartition.length === 0) {
- return annotatedBucketsBySeverity;
- }
- const severityCategory = getSeverityCategoryForScore(
- Math.max(
- ...maxAnomalyScoresByPartition.map((partitionScore) => partitionScore.maximumAnomalyScore)
- )
- );
- if (!severityCategory) {
- return annotatedBucketsBySeverity;
- }
- const sortedMaxAnomalyScoresByPartition = maxAnomalyScoresByPartition.sort(
- (a, b) => b.maximumAnomalyScore - a.maximumAnomalyScore
- );
- return {
- ...annotatedBucketsBySeverity,
- [severityCategory]: [
- ...annotatedBucketsBySeverity[severityCategory],
- {
- coordinates: {
- x0: bucket.startTime,
- x1: bucket.startTime + results.bucketDuration,
- },
- details: JSON.stringify({
- anomalyScoresByPartition: sortedMaxAnomalyScoresByPartition,
- }),
- },
- ],
- };
- },
- {
- warning: [],
- minor: [],
- major: [],
- critical: [],
- }
- );
-};
-
-export const getTopAnomalyScoreAcrossAllPartitions = (results: LogEntryRateResults) => {
- const allTopScores = Object.values(results.partitionBuckets).reduce(
- (scores: number[], partition) => {
- return [...scores, partition.topAnomalyScore];
- },
- []
- );
- return Math.max(...allTopScores);
-};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts
deleted file mode 100644
index 4b677140e2a7a..0000000000000
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts
+++ /dev/null
@@ -1,43 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { HttpHandler } from 'src/core/public';
-import {
- getLogEntryRateRequestPayloadRT,
- getLogEntryRateSuccessReponsePayloadRT,
- LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH,
-} from '../../../../../common/http_api/log_analysis';
-import { decodeOrThrow } from '../../../../../common/runtime_types';
-
-interface RequestArgs {
- sourceId: string;
- startTime: number;
- endTime: number;
- bucketDuration: number;
- datasets?: string[];
-}
-
-export const callGetLogEntryRateAPI = async (requestArgs: RequestArgs, fetch: HttpHandler) => {
- const { sourceId, startTime, endTime, bucketDuration, datasets } = requestArgs;
- const response = await fetch(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, {
- method: 'POST',
- body: JSON.stringify(
- getLogEntryRateRequestPayloadRT.encode({
- data: {
- sourceId,
- timeRange: {
- startTime,
- endTime,
- },
- bucketDuration,
- datasets,
- },
- })
- ),
- });
- return decodeOrThrow(getLogEntryRateSuccessReponsePayloadRT)(response);
-};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts
new file mode 100644
index 0000000000000..9bd1e42779a36
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useEffect, useReducer, useCallback } from 'react';
+import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
+import { Filter } from '../../../../../../../src/plugins/data/common';
+import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../../../../ml/public';
+
+interface ReducerState {
+ selectedDatasets: string[];
+ selectedDatasetsFilters: Filter[];
+}
+
+type ReducerAction =
+ | { type: 'changeSelectedDatasets'; payload: { datasets: string[] } }
+ | { type: 'updateDatasetsFilters'; payload: { filters: Filter[] } };
+
+const initialState: ReducerState = {
+ selectedDatasets: [],
+ selectedDatasetsFilters: [],
+};
+
+function reducer(state: ReducerState, action: ReducerAction) {
+ switch (action.type) {
+ case 'changeSelectedDatasets':
+ return {
+ ...state,
+ selectedDatasets: action.payload.datasets,
+ };
+ case 'updateDatasetsFilters':
+ const datasetsToAdd = action.payload.filters
+ .filter((filter) => !state.selectedDatasets.includes(filter.meta.params.query))
+ .map((filter) => filter.meta.params.query);
+ return {
+ ...state,
+ selectedDatasets: [...state.selectedDatasets, ...datasetsToAdd],
+ selectedDatasetsFilters: action.payload.filters,
+ };
+ default:
+ throw new Error('Unknown action');
+ }
+}
+
+export const useDatasetFiltering = () => {
+ const { services } = useKibanaContextForPlugin();
+ const [reducerState, dispatch] = useReducer(reducer, initialState);
+
+ const handleSetSelectedDatasets = useCallback(
+ (datasets: string[]) => {
+ dispatch({ type: 'changeSelectedDatasets', payload: { datasets } });
+ },
+ [dispatch]
+ );
+
+ // NOTE: The anomaly swimlane embeddable will communicate it's filter action
+ // changes via the filterManager service.
+ useEffect(() => {
+ const sub = services.data.query.filterManager.getUpdates$().subscribe(() => {
+ const filters = services.data.query.filterManager
+ .getFilters()
+ .filter(
+ (filter) =>
+ filter.meta.controlledBy && filter.meta.controlledBy === CONTROLLED_BY_SWIM_LANE_FILTER
+ );
+ dispatch({ type: 'updateDatasetsFilters', payload: { filters } });
+ });
+
+ return () => sub.unsubscribe();
+ }, [services.data.query.filterManager, dispatch]);
+
+ // NOTE: When filters are removed via the UI we need to make sure these are also tidied up
+ // within the FilterManager service, otherwise a scenario can occur where that filter can't
+ // be re-added via the embeddable as it will be seen as a duplicate to the FilterManager,
+ // and no update will be emitted.
+ useEffect(() => {
+ const filtersToRemove = reducerState.selectedDatasetsFilters.filter(
+ (filter) => !reducerState.selectedDatasets.includes(filter.meta.params.query)
+ );
+ if (filtersToRemove.length > 0) {
+ filtersToRemove.forEach((filter) => {
+ services.data.query.filterManager.removeFilter(filter);
+ });
+ }
+ }, [
+ reducerState.selectedDatasets,
+ reducerState.selectedDatasetsFilters,
+ services.data.query.filterManager,
+ ]);
+
+ return {
+ selectedDatasets: reducerState.selectedDatasets,
+ setSelectedDatasets: handleSetSelectedDatasets,
+ selectedDatasetsFilters: reducerState.selectedDatasetsFilters,
+ };
+};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts
deleted file mode 100644
index a226977a30c57..0000000000000
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts
+++ /dev/null
@@ -1,160 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { useMemo, useState } from 'react';
-
-import {
- GetLogEntryRateSuccessResponsePayload,
- LogEntryRateHistogramBucket,
- LogEntryRatePartition,
- LogEntryRateAnomaly,
-} from '../../../../common/http_api/log_analysis';
-import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
-import { useTrackedPromise } from '../../../utils/use_tracked_promise';
-import { callGetLogEntryRateAPI } from './service_calls/get_log_entry_rate';
-
-type PartitionBucket = LogEntryRatePartition & {
- startTime: number;
-};
-
-type PartitionRecord = Record<
- string,
- { buckets: PartitionBucket[]; topAnomalyScore: number; totalNumberOfLogEntries: number }
->;
-
-export type AnomalyRecord = LogEntryRateAnomaly & {
- partitionId: string;
-};
-
-export interface LogEntryRateResults {
- bucketDuration: number;
- totalNumberOfLogEntries: number;
- histogramBuckets: LogEntryRateHistogramBucket[];
- partitionBuckets: PartitionRecord;
- anomalies: AnomalyRecord[];
-}
-
-export const useLogEntryRateResults = ({
- sourceId,
- startTime,
- endTime,
- bucketDuration = 15 * 60 * 1000,
- filteredDatasets,
-}: {
- sourceId: string;
- startTime: number;
- endTime: number;
- bucketDuration: number;
- filteredDatasets?: string[];
-}) => {
- const { services } = useKibanaContextForPlugin();
- const [logEntryRate, setLogEntryRate] = useState(null);
-
- const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise(
- {
- cancelPreviousOn: 'resolution',
- createPromise: async () => {
- return await callGetLogEntryRateAPI(
- {
- sourceId,
- startTime,
- endTime,
- bucketDuration,
- datasets: filteredDatasets,
- },
- services.http.fetch
- );
- },
- onResolve: ({ data }) => {
- setLogEntryRate({
- bucketDuration: data.bucketDuration,
- totalNumberOfLogEntries: data.totalNumberOfLogEntries,
- histogramBuckets: data.histogramBuckets,
- partitionBuckets: formatLogEntryRateResultsByPartition(data),
- anomalies: formatLogEntryRateResultsByAllAnomalies(data),
- });
- },
- onReject: () => {
- setLogEntryRate(null);
- },
- },
- [sourceId, startTime, endTime, bucketDuration, filteredDatasets]
- );
-
- const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [
- getLogEntryRateRequest.state,
- ]);
-
- return {
- getLogEntryRate,
- isLoading,
- logEntryRate,
- };
-};
-
-const formatLogEntryRateResultsByPartition = (
- results: GetLogEntryRateSuccessResponsePayload['data']
-): PartitionRecord => {
- const partitionedBuckets = results.histogramBuckets.reduce<
- Record
- >((partitionResults, bucket) => {
- return bucket.partitions.reduce>(
- (_partitionResults, partition) => {
- return {
- ..._partitionResults,
- [partition.partitionId]: {
- buckets: _partitionResults[partition.partitionId]
- ? [
- ..._partitionResults[partition.partitionId].buckets,
- { startTime: bucket.startTime, ...partition },
- ]
- : [{ startTime: bucket.startTime, ...partition }],
- },
- };
- },
- partitionResults
- );
- }, {});
-
- const resultsByPartition: PartitionRecord = {};
-
- Object.entries(partitionedBuckets).map(([key, value]) => {
- const anomalyScores = value.buckets.reduce((scores: number[], bucket) => {
- return [...scores, bucket.maximumAnomalyScore];
- }, []);
- const totalNumberOfLogEntries = value.buckets.reduce((total, bucket) => {
- return (total += bucket.numberOfLogEntries);
- }, 0);
- resultsByPartition[key] = {
- topAnomalyScore: Math.max(...anomalyScores),
- totalNumberOfLogEntries,
- buckets: value.buckets,
- };
- });
-
- return resultsByPartition;
-};
-
-const formatLogEntryRateResultsByAllAnomalies = (
- results: GetLogEntryRateSuccessResponsePayload['data']
-): AnomalyRecord[] => {
- return results.histogramBuckets.reduce((anomalies, bucket) => {
- return bucket.partitions.reduce((_anomalies, partition) => {
- if (partition.anomalies.length > 0) {
- partition.anomalies.forEach((anomaly) => {
- _anomalies.push({
- partitionId: partition.partitionId,
- ...anomaly,
- });
- });
- return _anomalies;
- } else {
- return _anomalies;
- }
- }, anomalies);
- }, []);
-};
diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx
index fdbde1acb83ad..ccfae14fd4a59 100644
--- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx
@@ -5,24 +5,32 @@
* 2.0.
*/
-import { fold } from 'fp-ts/lib/Either';
-import { constant, identity } from 'fp-ts/lib/function';
-import { pipe } from 'fp-ts/lib/pipeable';
+import { useCallback, useMemo, useState } from 'react';
+import datemath from '@elastic/datemath';
+import moment from 'moment';
import * as rt from 'io-ts';
-
+import { TimeRange as KibanaTimeRange } from '../../../../../../../src/plugins/data/public';
+import { TimeRange } from '../../../../common/time/time_range';
import { useUrlState } from '../../../utils/use_url_state';
+import { useInterval } from '../../../hooks/use_interval';
import {
useKibanaTimefilterTime,
useSyncKibanaTimeFilterTime,
} from '../../../hooks/use_kibana_timefilter_time';
+import { decodeOrThrow } from '../../../../common/runtime_types';
-const autoRefreshRT = rt.union([
- rt.type({
- interval: rt.number,
- isPaused: rt.boolean,
- }),
- rt.undefined,
-]);
+const autoRefreshRT = rt.type({
+ interval: rt.number,
+ isPaused: rt.boolean,
+});
+
+export type AutoRefresh = rt.TypeOf;
+const urlAutoRefreshRT = rt.union([autoRefreshRT, rt.undefined]);
+const decodeAutoRefreshUrlState = decodeOrThrow(urlAutoRefreshRT);
+const defaultAutoRefreshState = {
+ isPaused: false,
+ interval: 30000,
+};
export const stringTimeRangeRT = rt.type({
startTime: rt.string,
@@ -31,6 +39,7 @@ export const stringTimeRangeRT = rt.type({
export type StringTimeRange = rt.TypeOf;
const urlTimeRangeRT = rt.union([stringTimeRangeRT, rt.undefined]);
+const decodeTimeRangeUrlState = decodeOrThrow(urlTimeRangeRT);
const TIME_RANGE_URL_STATE_KEY = 'timeRange';
const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh';
@@ -40,36 +49,102 @@ export const useLogAnalysisResultsUrlState = () => {
const [getTime] = useKibanaTimefilterTime(TIME_DEFAULTS);
const { from: start, to: end } = getTime();
- const [timeRange, setTimeRange] = useUrlState({
- defaultState: {
+ const defaultTimeRangeState = useMemo(() => {
+ return {
startTime: start,
endTime: end,
- },
- decodeUrlState: (value: unknown) =>
- pipe(urlTimeRangeRT.decode(value), fold(constant(undefined), identity)),
+ };
+ }, [start, end]);
+
+ const [urlTimeRange, setUrlTimeRange] = useUrlState({
+ defaultState: defaultTimeRangeState,
+ decodeUrlState: decodeTimeRangeUrlState,
encodeUrlState: urlTimeRangeRT.encode,
urlStateKey: TIME_RANGE_URL_STATE_KEY,
writeDefaultState: true,
});
- useSyncKibanaTimeFilterTime(TIME_DEFAULTS, { from: timeRange.startTime, to: timeRange.endTime });
+ // Numeric time range for querying APIs
+ const [queryTimeRange, setQueryTimeRange] = useState<{
+ value: TimeRange;
+ lastChangedTime: number;
+ }>(() => ({
+ value: stringToNumericTimeRange({ start: urlTimeRange.startTime, end: urlTimeRange.endTime }),
+ lastChangedTime: Date.now(),
+ }));
- const [autoRefresh, setAutoRefresh] = useUrlState({
- defaultState: {
- isPaused: false,
- interval: 30000,
+ const handleQueryTimeRangeChange = useCallback(
+ ({ start: startTime, end: endTime }: { start: string; end: string }) => {
+ setQueryTimeRange({
+ value: stringToNumericTimeRange({ start: startTime, end: endTime }),
+ lastChangedTime: Date.now(),
+ });
+ },
+ [setQueryTimeRange]
+ );
+
+ const setTimeRange = useCallback(
+ (selectedTime: { start: string; end: string }) => {
+ setUrlTimeRange({
+ startTime: selectedTime.start,
+ endTime: selectedTime.end,
+ });
+ handleQueryTimeRangeChange(selectedTime);
},
- decodeUrlState: (value: unknown) =>
- pipe(autoRefreshRT.decode(value), fold(constant(undefined), identity)),
- encodeUrlState: autoRefreshRT.encode,
+ [setUrlTimeRange, handleQueryTimeRangeChange]
+ );
+
+ const handleTimeFilterChange = useCallback(
+ (newTimeRange: KibanaTimeRange) => {
+ const { from, to } = newTimeRange;
+ setTimeRange({ start: from, end: to });
+ },
+ [setTimeRange]
+ );
+
+ useSyncKibanaTimeFilterTime(
+ TIME_DEFAULTS,
+ { from: urlTimeRange.startTime, to: urlTimeRange.endTime },
+ handleTimeFilterChange
+ );
+
+ const [autoRefresh, setAutoRefresh] = useUrlState({
+ defaultState: defaultAutoRefreshState,
+ decodeUrlState: decodeAutoRefreshUrlState,
+ encodeUrlState: urlAutoRefreshRT.encode,
urlStateKey: AUTOREFRESH_URL_STATE_KEY,
writeDefaultState: true,
});
+ useInterval(
+ () => {
+ handleQueryTimeRangeChange({
+ start: urlTimeRange.startTime,
+ end: urlTimeRange.endTime,
+ });
+ },
+ autoRefresh.isPaused ? null : autoRefresh.interval
+ );
+
return {
- timeRange,
+ timeRange: queryTimeRange,
+ friendlyTimeRange: urlTimeRange,
setTimeRange,
autoRefresh,
setAutoRefresh,
};
};
+
+const stringToNumericTimeRange = (timeRange: { start: string; end: string }): TimeRange => ({
+ startTime: moment(
+ datemath.parse(timeRange.start, {
+ momentInstance: moment,
+ })
+ ).valueOf(),
+ endTime: moment(
+ datemath.parse(timeRange.end, {
+ momentInstance: moment,
+ roundUp: true,
+ })
+ ).valueOf(),
+});
diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx
index 52c2a70f2d359..8fd32bda7fbc8 100644
--- a/x-pack/plugins/infra/public/pages/metrics/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx
@@ -35,12 +35,11 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options
import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time';
import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters';
-import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown';
-import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown';
+import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown';
import { SavedView } from '../../containers/saved_view/saved_view';
import { AlertPrefillProvider } from '../../alerting/use_alert_prefill';
import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities';
-import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout';
+import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout';
import { HeaderMenuPortal } from '../../../../observability/public';
import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider';
@@ -83,8 +82,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
-
-
+
{
jobSummaries: k8sJobSummaries,
} = useMetricK8sModuleContext();
const {
- hasInfraMLCapabilites,
+ hasInfraMLCapabilities,
hasInfraMLReadCapabilities,
hasInfraMLSetupCapabilities,
} = useInfraMLCapabilitiesContext();
@@ -69,7 +69,7 @@ export const FlyoutHome = (props: Props) => {
}
}, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]);
- if (!hasInfraMLCapabilites) {
+ if (!hasInfraMLCapabilities) {
return ;
} else if (!hasInfraMLReadCapabilities) {
return ;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx
index a6a296f7d5725..0248241d616dc 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx
@@ -51,7 +51,7 @@ interface Props {
}
export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible }) => {
- const { sourceId } = useSourceContext();
+ const { sourceId, source } = useSourceContext();
const { metric, nodeType, accountId, region } = useWaffleOptionsContext();
const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext();
const { filterQueryAsJson } = useWaffleFiltersContext();
@@ -70,6 +70,7 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible
const anomalyParams = {
sourceId: 'default',
+ anomalyThreshold: source?.configuration.anomalyThreshold || 0,
startTime,
endTime,
defaultSortOptions: {
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts
index c3732fb22cb63..25afd05633fa5 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts
@@ -138,6 +138,7 @@ export const useMetricsHostsAnomaliesResults = ({
endTime,
startTime,
sourceId,
+ anomalyThreshold,
defaultSortOptions,
defaultPaginationOptions,
onGetMetricsHostsAnomaliesDatasetsError,
@@ -146,6 +147,7 @@ export const useMetricsHostsAnomaliesResults = ({
endTime: number;
startTime: number;
sourceId: string;
+ anomalyThreshold: number;
defaultSortOptions: Sort;
defaultPaginationOptions: Pick;
onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void;
@@ -182,6 +184,7 @@ export const useMetricsHostsAnomaliesResults = ({
return await callGetMetricHostsAnomaliesAPI(
{
sourceId,
+ anomalyThreshold,
startTime: queryStartTime,
endTime: queryEndTime,
metric,
@@ -215,6 +218,7 @@ export const useMetricsHostsAnomaliesResults = ({
},
[
sourceId,
+ anomalyThreshold,
dispatch,
reducerState.timeRange,
reducerState.sortOptions,
@@ -296,6 +300,7 @@ export const useMetricsHostsAnomaliesResults = ({
interface RequestArgs {
sourceId: string;
+ anomalyThreshold: number;
startTime: number;
endTime: number;
metric: Metric;
@@ -307,13 +312,14 @@ export const callGetMetricHostsAnomaliesAPI = async (
requestArgs: RequestArgs,
fetch: HttpHandler
) => {
- const { sourceId, startTime, endTime, metric, sort, pagination } = requestArgs;
+ const { sourceId, anomalyThreshold, startTime, endTime, metric, sort, pagination } = requestArgs;
const response = await fetch(INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, {
method: 'POST',
body: JSON.stringify(
getMetricsHostsAnomaliesRequestPayloadRT.encode({
data: {
sourceId,
+ anomalyThreshold,
timeRange: {
startTime,
endTime,
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts
index 2a8beeaa814fc..c135a2c5e6661 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts
@@ -138,6 +138,7 @@ export const useMetricsK8sAnomaliesResults = ({
endTime,
startTime,
sourceId,
+ anomalyThreshold,
defaultSortOptions,
defaultPaginationOptions,
onGetMetricsHostsAnomaliesDatasetsError,
@@ -146,6 +147,7 @@ export const useMetricsK8sAnomaliesResults = ({
endTime: number;
startTime: number;
sourceId: string;
+ anomalyThreshold: number;
defaultSortOptions: Sort;
defaultPaginationOptions: Pick;
onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void;
@@ -183,6 +185,7 @@ export const useMetricsK8sAnomaliesResults = ({
return await callGetMetricsK8sAnomaliesAPI(
{
sourceId,
+ anomalyThreshold,
startTime: queryStartTime,
endTime: queryEndTime,
metric,
@@ -217,6 +220,7 @@ export const useMetricsK8sAnomaliesResults = ({
},
[
sourceId,
+ anomalyThreshold,
dispatch,
reducerState.timeRange,
reducerState.sortOptions,
@@ -298,6 +302,7 @@ export const useMetricsK8sAnomaliesResults = ({
interface RequestArgs {
sourceId: string;
+ anomalyThreshold: number;
startTime: number;
endTime: number;
metric: Metric;
@@ -310,13 +315,23 @@ export const callGetMetricsK8sAnomaliesAPI = async (
requestArgs: RequestArgs,
fetch: HttpHandler
) => {
- const { sourceId, startTime, endTime, metric, sort, pagination, datasets } = requestArgs;
+ const {
+ sourceId,
+ anomalyThreshold,
+ startTime,
+ endTime,
+ metric,
+ sort,
+ pagination,
+ datasets,
+ } = requestArgs;
const response = await fetch(INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH, {
method: 'POST',
body: JSON.stringify(
getMetricsK8sAnomaliesRequestPayloadRT.encode({
data: {
sourceId,
+ anomalyThreshold,
timeRange: {
startTime,
endTime,
diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx
index 44391568741f3..e22c6fa661181 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx
@@ -10,7 +10,19 @@ import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion';
import { AutocompleteField } from '../../../../components/autocomplete_field';
-import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public';
+import {
+ esKuery,
+ IIndexPattern,
+ QuerySuggestion,
+} from '../../../../../../../../src/plugins/data/public';
+
+type LoadSuggestionsFn = (
+ e: string,
+ p: number,
+ m?: number,
+ transform?: (s: QuerySuggestion[]) => QuerySuggestion[]
+) => void;
+export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn;
interface Props {
derivedIndexPattern: IIndexPattern;
@@ -18,6 +30,7 @@ interface Props {
onChange?: (query: string) => void;
value?: string | null;
placeholder?: string;
+ curryLoadSuggestions?: CurryLoadSuggestionsType;
}
function validateQuery(query: string) {
@@ -35,6 +48,7 @@ export const MetricsExplorerKueryBar = ({
onChange,
value,
placeholder,
+ curryLoadSuggestions = defaultCurryLoadSuggestions,
}: Props) => {
const [draftQuery, setDraftQuery] = useState(value || '');
const [isValid, setValidation] = useState(true);
@@ -73,7 +87,7 @@ export const MetricsExplorerKueryBar = ({
aria-label={placeholder}
isLoadingSuggestions={isLoadingSuggestions}
isValid={isValid}
- loadSuggestions={loadSuggestions}
+ loadSuggestions={curryLoadSuggestions(loadSuggestions)}
onChange={handleChange}
onSubmit={onSubmit}
placeholder={placeholder || defaultPlaceholder}
@@ -84,3 +98,6 @@ export const MetricsExplorerKueryBar = ({
);
};
+
+const defaultCurryLoadSuggestions: CurryLoadSuggestionsType = (loadSuggestions) => (...args) =>
+ loadSuggestions(...args);
diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts
index 8e7d165f8a535..d4bb83e8668ba 100644
--- a/x-pack/plugins/infra/public/plugin.ts
+++ b/x-pack/plugins/infra/public/plugin.ts
@@ -10,6 +10,7 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
import { createInventoryMetricAlertType } from './alerting/inventory';
+import { createMetricAnomalyAlertType } from './alerting/metric_anomaly';
import { getAlertType as getLogsAlertType } from './alerting/log_threshold';
import { registerFeatures } from './register_feature';
import {
@@ -35,6 +36,7 @@ export class Plugin implements InfraClientPluginClass {
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType());
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType());
pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType());
+ pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricAnomalyAlertType());
if (pluginsSetup.observability) {
pluginsSetup.observability.dashboard.register({
diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts
index b78912bfba3ac..4d70676d25e40 100644
--- a/x-pack/plugins/infra/public/types.ts
+++ b/x-pack/plugins/infra/public/types.ts
@@ -23,7 +23,8 @@ import type {
ObservabilityPluginStart,
} from '../../observability/public';
import type { SpacesPluginStart } from '../../spaces/public';
-import { MlPluginStart } from '../../ml/public';
+import { MlPluginStart, MlPluginSetup } from '../../ml/public';
+import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
// Our own setup and start contract values
export type InfraClientSetupExports = void;
@@ -35,6 +36,7 @@ export interface InfraClientSetupDeps {
observability: ObservabilityPluginSetup;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
usageCollection: UsageCollectionSetup;
+ ml: MlPluginSetup;
embeddable: EmbeddableSetup;
}
@@ -46,6 +48,7 @@ export interface InfraClientStartDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
usageCollection: UsageCollectionStart;
ml: MlPluginStart;
+ embeddable?: EmbeddableStart;
}
export type InfraClientCoreSetup = CoreSetup;
diff --git a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts
index 6d8f9ae476044..27648b6d7b193 100644
--- a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts
+++ b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts
@@ -40,6 +40,7 @@ export const source = {
message: ['message'],
tiebreaker: '@timestamp',
},
+ anomalyThreshold: 20,
};
export const chartOptions: MetricsExplorerChartOptions = {
diff --git a/x-pack/plugins/infra/public/utils/use_url_state.ts b/x-pack/plugins/infra/public/utils/use_url_state.ts
index fd927bb5ef662..970b3a20b2951 100644
--- a/x-pack/plugins/infra/public/utils/use_url_state.ts
+++ b/x-pack/plugins/infra/public/utils/use_url_state.ts
@@ -38,15 +38,13 @@ export const useUrlState = ({
return getParamFromQueryString(queryString, urlStateKey);
}, [queryString, urlStateKey]);
- const decodedState = useMemo(() => decodeUrlState(decodeRisonUrlState(urlStateString)), [
- decodeUrlState,
- urlStateString,
- ]);
-
- const state = useMemo(() => (typeof decodedState !== 'undefined' ? decodedState : defaultState), [
- defaultState,
- decodedState,
- ]);
+ const decodedState = useMemo(() => {
+ return decodeUrlState(decodeRisonUrlState(urlStateString));
+ }, [decodeUrlState, urlStateString]);
+
+ const state = useMemo(() => {
+ return typeof decodedState !== 'undefined' ? decodedState : defaultState;
+ }, [defaultState, decodedState]);
const setState = useCallback(
(newState: State | undefined) => {
diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts
index 00ec36d866624..8a6f22d55750e 100644
--- a/x-pack/plugins/infra/server/infra_server.ts
+++ b/x-pack/plugins/infra/server/infra_server.ts
@@ -12,7 +12,6 @@ import {
initGetLogEntryCategoryDatasetsRoute,
initGetLogEntryCategoryDatasetsStatsRoute,
initGetLogEntryCategoryExamplesRoute,
- initGetLogEntryRateRoute,
initGetLogEntryExamplesRoute,
initValidateLogAnalysisDatasetsRoute,
initValidateLogAnalysisIndicesRoute,
@@ -46,7 +45,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initGetLogEntryCategoryDatasetsRoute(libs);
initGetLogEntryCategoryDatasetsStatsRoute(libs);
initGetLogEntryCategoryExamplesRoute(libs);
- initGetLogEntryRateRoute(libs);
initGetLogEntryAnomaliesRoute(libs);
initGetLogEntryAnomaliesDatasetsRoute(libs);
initGetK8sAnomaliesRoute(libs);
diff --git a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
index 9f0be1679448f..b692629209849 100644
--- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
@@ -19,6 +19,9 @@ export const stateToAlertMessage = {
[AlertStates.ALERT]: i18n.translate('xpack.infra.metrics.alerting.threshold.alertState', {
defaultMessage: 'ALERT',
}),
+ [AlertStates.WARNING]: i18n.translate('xpack.infra.metrics.alerting.threshold.warningState', {
+ defaultMessage: 'WARNING',
+ }),
[AlertStates.NO_DATA]: i18n.translate('xpack.infra.metrics.alerting.threshold.noDataState', {
defaultMessage: 'NO DATA',
}),
diff --git a/x-pack/plugins/infra/server/lib/alerting/common/types.ts b/x-pack/plugins/infra/server/lib/alerting/common/types.ts
index e4db2600e316d..0b809429de0d2 100644
--- a/x-pack/plugins/infra/server/lib/alerting/common/types.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/common/types.ts
@@ -29,6 +29,15 @@ export enum Aggregators {
export enum AlertStates {
OK,
ALERT,
+ WARNING,
NO_DATA,
ERROR,
}
+
+export interface PreviewResult {
+ fired: number;
+ warning: number;
+ noData: number;
+ error: number;
+ notifications: number;
+}
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
index 16f74d579969a..ea37f7adda7c4 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
@@ -26,6 +26,7 @@ import { getNodes } from '../../../routes/snapshot/lib/get_nodes';
type ConditionResult = InventoryMetricConditions & {
shouldFire: boolean[];
+ shouldWarn: boolean[];
currentValue: number;
isNoData: boolean[];
isError: boolean;
@@ -39,8 +40,8 @@ export const evaluateCondition = async (
filterQuery?: string,
lookbackSize?: number
): Promise> => {
- const { comparator, metric, customMetric } = condition;
- let { threshold } = condition;
+ const { comparator, warningComparator, metric, customMetric } = condition;
+ let { threshold, warningThreshold } = condition;
const timerange = {
to: Date.now(),
@@ -62,19 +63,22 @@ export const evaluateCondition = async (
);
threshold = threshold.map((n) => convertMetricValue(metric, n));
-
- const comparisonFunction = comparatorMap[comparator];
+ warningThreshold = warningThreshold?.map((n) => convertMetricValue(metric, n));
+
+ const valueEvaluator = (value?: DataValue, t?: number[], c?: Comparator) => {
+ if (value === undefined || value === null || !t || !c) return [false];
+ const comparisonFunction = comparatorMap[c];
+ return Array.isArray(value)
+ ? value.map((v) => comparisonFunction(Number(v), t))
+ : [comparisonFunction(value as number, t)];
+ };
const result = mapValues(currentValues, (value) => {
if (isTooManyBucketsPreviewException(value)) throw value;
return {
...condition,
- shouldFire:
- value !== undefined &&
- value !== null &&
- (Array.isArray(value)
- ? value.map((v) => comparisonFunction(Number(v), threshold))
- : [comparisonFunction(value as number, threshold)]),
+ shouldFire: valueEvaluator(value, threshold, comparator),
+ shouldWarn: valueEvaluator(value, warningThreshold, warningComparator),
isNoData: Array.isArray(value) ? value.map((v) => v === null) : [value === null],
isError: value === undefined,
currentValue: getCurrentValue(value),
@@ -90,6 +94,7 @@ const getCurrentValue: (value: any) => number = (value) => {
return NaN;
};
+type DataValue = number | null | Array;
const getData = async (
callCluster: AlertServices['callCluster'],
nodeType: InventoryItemType,
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index 2658fa6820274..a15f1010194a5 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -81,6 +81,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
// Grab the result of the most recent bucket
last(result[item].shouldFire)
);
+ const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn));
// AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state
@@ -93,12 +94,20 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
? AlertStates.NO_DATA
: shouldAlertFire
? AlertStates.ALERT
+ : shouldAlertWarn
+ ? AlertStates.WARNING
: AlertStates.OK;
let reason;
- if (nextState === AlertStates.ALERT) {
+ if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) {
reason = results
- .map((result) => buildReasonWithVerboseMetricName(result[item], buildFiredAlertReason))
+ .map((result) =>
+ buildReasonWithVerboseMetricName(
+ result[item],
+ buildFiredAlertReason,
+ nextState === AlertStates.WARNING
+ )
+ )
.join('\n');
} else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) {
/*
@@ -125,7 +134,11 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
}
if (reason) {
const actionGroupId =
- nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS_ID;
+ nextState === AlertStates.OK
+ ? RecoveredActionGroup.id
+ : nextState === AlertStates.WARNING
+ ? WARNING_ACTIONS.id
+ : FIRED_ACTIONS.id;
alertInstance.scheduleActions(
/**
* TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on
@@ -152,7 +165,11 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
}
};
-const buildReasonWithVerboseMetricName = (resultItem: any, buildReason: (r: any) => string) => {
+const buildReasonWithVerboseMetricName = (
+ resultItem: any,
+ buildReason: (r: any) => string,
+ useWarningThreshold?: boolean
+) => {
if (!resultItem) return '';
const resultWithVerboseMetricName = {
...resultItem,
@@ -162,6 +179,8 @@ const buildReasonWithVerboseMetricName = (resultItem: any, buildReason: (r: any)
? getCustomMetricLabel(resultItem.customMetric)
: resultItem.metric),
currentValue: formatMetric(resultItem.metric, resultItem.currentValue),
+ threshold: useWarningThreshold ? resultItem.warningThreshold! : resultItem.threshold,
+ comparator: useWarningThreshold ? resultItem.warningComparator! : resultItem.comparator,
};
return buildReason(resultWithVerboseMetricName);
};
@@ -177,11 +196,18 @@ const mapToConditionsLookup = (
{}
);
-export const FIRED_ACTIONS_ID = 'metrics.invenotry_threshold.fired';
+export const FIRED_ACTIONS_ID = 'metrics.inventory_threshold.fired';
export const FIRED_ACTIONS: ActionGroup = {
id: FIRED_ACTIONS_ID,
name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', {
- defaultMessage: 'Fired',
+ defaultMessage: 'Alert',
+ }),
+};
+export const WARNING_ACTIONS_ID = 'metrics.inventory_threshold.warning';
+export const WARNING_ACTIONS = {
+ id: WARNING_ACTIONS_ID,
+ name: i18n.translate('xpack.infra.metrics.alerting.threshold.warning', {
+ defaultMessage: 'Warning',
}),
};
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
index 528c0f92d20e7..5fff76260e5c6 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
@@ -7,6 +7,7 @@
import { Unit } from '@elastic/datemath';
import { first } from 'lodash';
+import { PreviewResult } from '../common/types';
import { InventoryMetricConditions } from './types';
import {
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
@@ -35,7 +36,9 @@ interface PreviewInventoryMetricThresholdAlertParams {
alertOnNoData: boolean;
}
-export const previewInventoryMetricThresholdAlert = async ({
+export const previewInventoryMetricThresholdAlert: (
+ params: PreviewInventoryMetricThresholdAlertParams
+) => Promise = async ({
callCluster,
params,
source,
@@ -43,7 +46,7 @@ export const previewInventoryMetricThresholdAlert = async ({
alertInterval,
alertThrottle,
alertOnNoData,
-}: PreviewInventoryMetricThresholdAlertParams) => {
+}) => {
const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams;
if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions');
@@ -74,6 +77,7 @@ export const previewInventoryMetricThresholdAlert = async ({
const numberOfResultBuckets = lookbackSize;
const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution);
let numberOfTimesFired = 0;
+ let numberOfTimesWarned = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
let numberOfNotifications = 0;
@@ -88,6 +92,9 @@ export const previewInventoryMetricThresholdAlert = async ({
const shouldFire = result[item].shouldFire as boolean[];
return shouldFire[mappedBucketIndex];
});
+ const allConditionsWarnInMappedBucket =
+ !allConditionsFiredInMappedBucket &&
+ results.every((result) => result[item].shouldWarn[mappedBucketIndex]);
const someConditionsNoDataInMappedBucket = results.some((result) => {
const hasNoData = result[item].isNoData as boolean[];
return hasNoData[mappedBucketIndex];
@@ -108,6 +115,9 @@ export const previewInventoryMetricThresholdAlert = async ({
} else if (allConditionsFiredInMappedBucket) {
numberOfTimesFired++;
notifyWithThrottle();
+ } else if (allConditionsWarnInMappedBucket) {
+ numberOfTimesWarned++;
+ notifyWithThrottle();
} else if (throttleTracker > 0) {
throttleTracker++;
}
@@ -115,7 +125,13 @@ export const previewInventoryMetricThresholdAlert = async ({
throttleTracker = 0;
}
}
- return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications];
+ return {
+ fired: numberOfTimesFired,
+ warning: numberOfTimesWarned,
+ noData: numberOfNoDataResults,
+ error: numberOfErrors,
+ notifications: numberOfNotifications,
+ };
});
return previewResults;
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
index 4ae1a0e4d5d49..6c439225d9d00 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
@@ -7,11 +7,17 @@
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
-import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server';
+import {
+ AlertType,
+ AlertInstanceState,
+ AlertInstanceContext,
+ ActionGroupIdsOf,
+} from '../../../../../alerts/server';
import {
createInventoryMetricThresholdExecutor,
FIRED_ACTIONS,
FIRED_ACTIONS_ID,
+ WARNING_ACTIONS,
} from './inventory_metric_threshold_executor';
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types';
import { InfraBackendLibs } from '../../infra_types';
@@ -25,7 +31,6 @@ import {
metricActionVariableDescription,
thresholdActionVariableDescription,
} from '../common/messages';
-import { RecoveredActionGroupId } from '../../../../../alerts/common';
const condition = schema.object({
threshold: schema.arrayOf(schema.number()),
@@ -33,6 +38,8 @@ const condition = schema.object({
timeUnit: schema.string(),
timeSize: schema.number(),
metric: schema.string(),
+ warningThreshold: schema.maybe(schema.arrayOf(schema.number())),
+ warningComparator: schema.maybe(oneOfLiterals(Object.values(Comparator))),
customMetric: schema.maybe(
schema.object({
type: schema.literal('custom'),
@@ -44,7 +51,9 @@ const condition = schema.object({
),
});
-export type InventoryMetricThresholdAllowedActionGroups = typeof FIRED_ACTIONS_ID;
+export type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf<
+ typeof FIRED_ACTIONS | typeof WARNING_ACTIONS
+>;
export const registerMetricInventoryThresholdAlertType = (
libs: InfraBackendLibs
@@ -56,8 +65,7 @@ export const registerMetricInventoryThresholdAlertType = (
Record,
AlertInstanceState,
AlertInstanceContext,
- InventoryMetricThresholdAllowedActionGroups,
- RecoveredActionGroupId
+ InventoryMetricThresholdAllowedActionGroups
> => ({
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
name: i18n.translate('xpack.infra.metrics.inventory.alertName', {
@@ -78,7 +86,7 @@ export const registerMetricInventoryThresholdAlertType = (
),
},
defaultActionGroupId: FIRED_ACTIONS_ID,
- actionGroups: [FIRED_ACTIONS],
+ actionGroups: [FIRED_ACTIONS, WARNING_ACTIONS],
producer: 'infrastructure',
minimumLicenseRequired: 'basic',
executor: createInventoryMetricThresholdExecutor(libs),
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts
index 28c41de9b10d6..120fa47c079ab 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts
@@ -22,4 +22,6 @@ export interface InventoryMetricConditions {
threshold: number[];
comparator: Comparator;
customMetric?: SnapshotCustomMetricInput;
+ warningThreshold?: number[];
+ warningComparator?: Comparator;
}
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts
new file mode 100644
index 0000000000000..b7ef8ec7d2312
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/evaluate_condition.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
+import { getMetricsHostsAnomalies, getMetricK8sAnomalies } from '../../infra_ml';
+import { MlSystem, MlAnomalyDetectors } from '../../../types';
+
+type ConditionParams = Omit & {
+ spaceId: string;
+ startTime: number;
+ endTime: number;
+ mlSystem: MlSystem;
+ mlAnomalyDetectors: MlAnomalyDetectors;
+};
+
+export const evaluateCondition = async ({
+ nodeType,
+ spaceId,
+ sourceId,
+ mlSystem,
+ mlAnomalyDetectors,
+ startTime,
+ endTime,
+ metric,
+ threshold,
+ influencerFilter,
+}: ConditionParams) => {
+ const getAnomalies = nodeType === 'k8s' ? getMetricK8sAnomalies : getMetricsHostsAnomalies;
+
+ const result = await getAnomalies(
+ {
+ spaceId,
+ mlSystem,
+ mlAnomalyDetectors,
+ },
+ sourceId ?? 'default',
+ threshold,
+ startTime,
+ endTime,
+ metric,
+ { field: 'anomalyScore', direction: 'desc' },
+ { pageSize: 100 },
+ influencerFilter
+ );
+
+ return result;
+};
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts
new file mode 100644
index 0000000000000..ec95aac7268ad
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { first } from 'lodash';
+import moment from 'moment';
+import { stateToAlertMessage } from '../common/messages';
+import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
+import { MappedAnomalyHit } from '../../infra_ml';
+import { AlertStates } from '../common/types';
+import {
+ ActionGroup,
+ AlertInstanceContext,
+ AlertInstanceState,
+} from '../../../../../alerts/common';
+import { AlertExecutorOptions } from '../../../../../alerts/server';
+import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
+import { MetricAnomalyAllowedActionGroups } from './register_metric_anomaly_alert_type';
+import { MlPluginSetup } from '../../../../../ml/server';
+import { KibanaRequest } from '../../../../../../../src/core/server';
+import { InfraBackendLibs } from '../../infra_types';
+import { evaluateCondition } from './evaluate_condition';
+
+export const createMetricAnomalyExecutor = (libs: InfraBackendLibs, ml?: MlPluginSetup) => async ({
+ services,
+ params,
+ startedAt,
+}: AlertExecutorOptions<
+ /**
+ * TODO: Remove this use of `any` by utilizing a proper type
+ */
+ Record,
+ Record,
+ AlertInstanceState,
+ AlertInstanceContext,
+ MetricAnomalyAllowedActionGroups
+>) => {
+ if (!ml) {
+ return;
+ }
+ const request = {} as KibanaRequest;
+ const mlSystem = ml.mlSystemProvider(request, services.savedObjectsClient);
+ const mlAnomalyDetectors = ml.anomalyDetectorsProvider(request, services.savedObjectsClient);
+
+ const {
+ metric,
+ alertInterval,
+ influencerFilter,
+ sourceId,
+ nodeType,
+ threshold,
+ } = params as MetricAnomalyParams;
+
+ const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`);
+
+ const bucketInterval = getIntervalInSeconds('15m') * 1000;
+ const alertIntervalInMs = getIntervalInSeconds(alertInterval ?? '1m') * 1000;
+
+ const endTime = startedAt.getTime();
+ // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour
+ const previousBucketStartTime = endTime - (endTime % bucketInterval);
+
+ // If the alert interval is less than 15m, make sure that it actually queries an anomaly bucket
+ const startTime = Math.min(endTime - alertIntervalInMs, previousBucketStartTime);
+
+ const { data } = await evaluateCondition({
+ sourceId: sourceId ?? 'default',
+ spaceId: 'default',
+ mlSystem,
+ mlAnomalyDetectors,
+ startTime,
+ endTime,
+ metric,
+ threshold,
+ nodeType,
+ influencerFilter,
+ });
+
+ const shouldAlertFire = data.length > 0;
+
+ if (shouldAlertFire) {
+ const { startTime: anomalyStartTime, anomalyScore, actual, typical, influencers } = first(
+ data as MappedAnomalyHit[]
+ )!;
+
+ alertInstance.scheduleActions(FIRED_ACTIONS_ID, {
+ alertState: stateToAlertMessage[AlertStates.ALERT],
+ timestamp: moment(anomalyStartTime).toISOString(),
+ anomalyScore,
+ actual,
+ typical,
+ metric: metricNameMap[metric],
+ summary: generateSummaryMessage(actual, typical),
+ influencers: influencers.join(', '),
+ });
+ }
+};
+
+export const FIRED_ACTIONS_ID = 'metrics.anomaly.fired';
+export const FIRED_ACTIONS: ActionGroup = {
+ id: FIRED_ACTIONS_ID,
+ name: i18n.translate('xpack.infra.metrics.alerting.anomaly.fired', {
+ defaultMessage: 'Fired',
+ }),
+};
+
+const generateSummaryMessage = (actual: number, typical: number) => {
+ const differential = (Math.max(actual, typical) / Math.min(actual, typical))
+ .toFixed(1)
+ .replace('.0', '');
+ if (actual > typical) {
+ return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryHigher', {
+ defaultMessage: '{differential}x higher',
+ values: {
+ differential,
+ },
+ });
+ } else {
+ return i18n.translate('xpack.infra.metrics.alerting.anomaly.summaryLower', {
+ defaultMessage: '{differential}x lower',
+ values: {
+ differential,
+ },
+ });
+ }
+};
+
+const metricNameMap = {
+ memory_usage: i18n.translate('xpack.infra.metrics.alerting.anomaly.memoryUsage', {
+ defaultMessage: 'Memory usage',
+ }),
+ network_in: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkIn', {
+ defaultMessage: 'Network in',
+ }),
+ network_out: i18n.translate('xpack.infra.metrics.alerting.anomaly.networkOut', {
+ defaultMessage: 'Network out',
+ }),
+};
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts
new file mode 100644
index 0000000000000..98992701e3bb4
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/preview_metric_anomaly_alert.ts
@@ -0,0 +1,120 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Unit } from '@elastic/datemath';
+import { countBy } from 'lodash';
+import { MappedAnomalyHit } from '../../infra_ml';
+import { MlSystem, MlAnomalyDetectors } from '../../../types';
+import { MetricAnomalyParams } from '../../../../common/alerting/metrics';
+import {
+ TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
+ isTooManyBucketsPreviewException,
+} from '../../../../common/alerting/metrics';
+import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
+import { evaluateCondition } from './evaluate_condition';
+
+interface PreviewMetricAnomalyAlertParams {
+ mlSystem: MlSystem;
+ mlAnomalyDetectors: MlAnomalyDetectors;
+ spaceId: string;
+ params: MetricAnomalyParams;
+ sourceId: string;
+ lookback: Unit;
+ alertInterval: string;
+ alertThrottle: string;
+ alertOnNoData: boolean;
+}
+
+export const previewMetricAnomalyAlert = async ({
+ mlSystem,
+ mlAnomalyDetectors,
+ spaceId,
+ params,
+ sourceId,
+ lookback,
+ alertInterval,
+ alertThrottle,
+}: PreviewMetricAnomalyAlertParams) => {
+ const { metric, threshold, influencerFilter, nodeType } = params as MetricAnomalyParams;
+
+ const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
+ const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle);
+ const executionsPerThrottle = Math.floor(throttleIntervalInSeconds / alertIntervalInSeconds);
+
+ const lookbackInterval = `1${lookback}`;
+ const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval);
+ const endTime = Date.now();
+ const startTime = endTime - lookbackIntervalInSeconds * 1000;
+
+ const numberOfExecutions = Math.floor(lookbackIntervalInSeconds / alertIntervalInSeconds);
+ const bucketIntervalInSeconds = getIntervalInSeconds('15m');
+ const bucketsPerExecution = Math.max(
+ 1,
+ Math.floor(alertIntervalInSeconds / bucketIntervalInSeconds)
+ );
+
+ try {
+ let anomalies: MappedAnomalyHit[] = [];
+ const { data } = await evaluateCondition({
+ nodeType,
+ spaceId,
+ sourceId,
+ mlSystem,
+ mlAnomalyDetectors,
+ startTime,
+ endTime,
+ metric,
+ threshold,
+ influencerFilter,
+ });
+ anomalies = [...anomalies, ...data];
+
+ const anomaliesByTime = countBy(anomalies, ({ startTime: anomStartTime }) => anomStartTime);
+
+ let numberOfTimesFired = 0;
+ let numberOfNotifications = 0;
+ let throttleTracker = 0;
+ const notifyWithThrottle = () => {
+ if (throttleTracker === 0) numberOfNotifications++;
+ throttleTracker++;
+ };
+ // Mock each alert evaluation
+ for (let i = 0; i < numberOfExecutions; i++) {
+ const executionTime = startTime + alertIntervalInSeconds * 1000 * i;
+ // Get an array of bucket times this mock alert evaluation will be looking at
+ // Anomalies are bucketed at :00, :15, :30, :45 minutes every hour,
+ // so this is an array of how many of those times occurred between this evaluation
+ // and the previous one
+ const bucketsLookedAt = Array.from(Array(bucketsPerExecution), (_, idx) => {
+ const previousBucketStartTime =
+ executionTime -
+ (executionTime % (bucketIntervalInSeconds * 1000)) -
+ idx * bucketIntervalInSeconds * 1000;
+ return previousBucketStartTime;
+ });
+ const anomaliesDetectedInBuckets = bucketsLookedAt.some((bucketTime) =>
+ Reflect.has(anomaliesByTime, bucketTime)
+ );
+
+ if (anomaliesDetectedInBuckets) {
+ numberOfTimesFired++;
+ notifyWithThrottle();
+ } else if (throttleTracker > 0) {
+ throttleTracker++;
+ }
+ if (throttleTracker === executionsPerThrottle) {
+ throttleTracker = 0;
+ }
+ }
+
+ return { fired: numberOfTimesFired, notifications: numberOfNotifications };
+ } catch (e) {
+ if (!isTooManyBucketsPreviewException(e)) throw e;
+ const { maxBuckets } = e;
+ throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets}`);
+ }
+};
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts
new file mode 100644
index 0000000000000..8ac62c125515a
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { i18n } from '@kbn/i18n';
+import { MlPluginSetup } from '../../../../../ml/server';
+import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server';
+import {
+ createMetricAnomalyExecutor,
+ FIRED_ACTIONS,
+ FIRED_ACTIONS_ID,
+} from './metric_anomaly_executor';
+import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
+import { InfraBackendLibs } from '../../infra_types';
+import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils';
+import { alertStateActionVariableDescription } from '../common/messages';
+import { RecoveredActionGroupId } from '../../../../../alerts/common';
+
+export type MetricAnomalyAllowedActionGroups = typeof FIRED_ACTIONS_ID;
+
+export const registerMetricAnomalyAlertType = (
+ libs: InfraBackendLibs,
+ ml?: MlPluginSetup
+): AlertType<
+ /**
+ * TODO: Remove this use of `any` by utilizing a proper type
+ */
+ Record,
+ Record,
+ AlertInstanceState,
+ AlertInstanceContext,
+ MetricAnomalyAllowedActionGroups,
+ RecoveredActionGroupId
+> => ({
+ id: METRIC_ANOMALY_ALERT_TYPE_ID,
+ name: i18n.translate('xpack.infra.metrics.anomaly.alertName', {
+ defaultMessage: 'Infrastructure anomaly',
+ }),
+ validate: {
+ params: schema.object(
+ {
+ nodeType: oneOfLiterals(['hosts', 'k8s']),
+ alertInterval: schema.string(),
+ metric: oneOfLiterals(['memory_usage', 'network_in', 'network_out']),
+ threshold: schema.number(),
+ filterQuery: schema.maybe(
+ schema.string({ validate: validateIsStringElasticsearchJSONFilter })
+ ),
+ sourceId: schema.string(),
+ },
+ { unknowns: 'allow' }
+ ),
+ },
+ defaultActionGroupId: FIRED_ACTIONS_ID,
+ actionGroups: [FIRED_ACTIONS],
+ producer: 'infrastructure',
+ minimumLicenseRequired: 'basic',
+ executor: createMetricAnomalyExecutor(libs, ml),
+ actionVariables: {
+ context: [
+ { name: 'alertState', description: alertStateActionVariableDescription },
+ {
+ name: 'metric',
+ description: i18n.translate('xpack.infra.metrics.alerting.anomalyMetricDescription', {
+ defaultMessage: 'The metric name in the specified condition.',
+ }),
+ },
+ {
+ name: 'timestamp',
+ description: i18n.translate('xpack.infra.metrics.alerting.anomalyTimestampDescription', {
+ defaultMessage: 'A timestamp of when the anomaly was detected.',
+ }),
+ },
+ {
+ name: 'anomalyScore',
+ description: i18n.translate('xpack.infra.metrics.alerting.anomalyScoreDescription', {
+ defaultMessage: 'The exact severity score of the detected anomaly.',
+ }),
+ },
+ {
+ name: 'actual',
+ description: i18n.translate('xpack.infra.metrics.alerting.anomalyActualDescription', {
+ defaultMessage: 'The actual value of the monitored metric at the time of the anomaly.',
+ }),
+ },
+ {
+ name: 'typical',
+ description: i18n.translate('xpack.infra.metrics.alerting.anomalyTypicalDescription', {
+ defaultMessage: 'The typical value of the monitored metric at the time of the anomaly.',
+ }),
+ },
+ {
+ name: 'summary',
+ description: i18n.translate('xpack.infra.metrics.alerting.anomalySummaryDescription', {
+ defaultMessage: 'A description of the anomaly, e.g. "2x higher."',
+ }),
+ },
+ {
+ name: 'influencers',
+ description: i18n.translate('xpack.infra.metrics.alerting.anomalyInfluencersDescription', {
+ defaultMessage: 'A list of node names that influenced the anomaly.',
+ }),
+ },
+ ],
+ },
+});
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
index f9661e2cd56bb..029445a441eea 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
@@ -60,8 +60,17 @@ export const evaluateAlert = {
+ if (!t || !c) return [false];
+ const comparisonFunction = comparatorMap[c];
+ return Array.isArray(points)
+ ? points.map(
+ (point) => t && typeof point.value === 'number' && comparisonFunction(point.value, t)
+ )
+ : [false];
+ };
+
return mapValues(currentValues, (points: any[] | typeof NaN | null) => {
if (isTooManyBucketsPreviewException(points)) throw points;
return {
@@ -69,12 +78,8 @@ export const evaluateAlert =
- typeof point.value === 'number' && comparisonFunction(point.value, threshold)
- )
- : [false],
+ shouldFire: pointsEvaluator(points, threshold, comparator),
+ shouldWarn: pointsEvaluator(points, warningThreshold, warningComparator),
isNoData: Array.isArray(points)
? points.map((point) => point?.value === null || point === null)
: [points === null],
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index 17b9ab1cab907..b822d71b3f812 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -18,7 +18,7 @@ import {
stateToAlertMessage,
} from '../common/messages';
import { createFormatter } from '../../../../common/formatters';
-import { AlertStates } from './types';
+import { AlertStates, Comparator } from './types';
import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert';
import {
MetricThresholdAlertExecutorOptions,
@@ -60,6 +60,7 @@ export const createMetricThresholdExecutor = (
// Grab the result of the most recent bucket
last(result[group].shouldFire)
);
+ const shouldAlertWarn = alertResults.every((result) => last(result[group].shouldWarn));
// AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state
const isNoData = alertResults.some((result) => last(result[group].isNoData));
@@ -71,12 +72,18 @@ export const createMetricThresholdExecutor = (
? AlertStates.NO_DATA
: shouldAlertFire
? AlertStates.ALERT
+ : shouldAlertWarn
+ ? AlertStates.WARNING
: AlertStates.OK;
let reason;
- if (nextState === AlertStates.ALERT) {
+ if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) {
reason = alertResults
- .map((result) => buildFiredAlertReason(formatAlertResult(result[group])))
+ .map((result) =>
+ buildFiredAlertReason(
+ formatAlertResult(result[group], nextState === AlertStates.WARNING)
+ )
+ )
.join('\n');
} else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) {
/*
@@ -105,7 +112,11 @@ export const createMetricThresholdExecutor = (
const firstResult = first(alertResults);
const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString();
const actionGroupId =
- nextState === AlertStates.OK ? RecoveredActionGroup.id : FIRED_ACTIONS.id;
+ nextState === AlertStates.OK
+ ? RecoveredActionGroup.id
+ : nextState === AlertStates.WARNING
+ ? WARNING_ACTIONS.id
+ : FIRED_ACTIONS.id;
alertInstance.scheduleActions(actionGroupId, {
group,
alertState: stateToAlertMessage[nextState],
@@ -132,7 +143,14 @@ export const createMetricThresholdExecutor = (
export const FIRED_ACTIONS = {
id: 'metrics.threshold.fired',
name: i18n.translate('xpack.infra.metrics.alerting.threshold.fired', {
- defaultMessage: 'Fired',
+ defaultMessage: 'Alert',
+ }),
+};
+
+export const WARNING_ACTIONS = {
+ id: 'metrics.threshold.warning',
+ name: i18n.translate('xpack.infra.metrics.alerting.threshold.warning', {
+ defaultMessage: 'Warning',
}),
};
@@ -152,9 +170,20 @@ const formatAlertResult = (
metric: string;
currentValue: number;
threshold: number[];
- } & AlertResult
+ comparator: Comparator;
+ warningThreshold?: number[];
+ warningComparator?: Comparator;
+ } & AlertResult,
+ useWarningThreshold?: boolean
) => {
- const { metric, currentValue, threshold } = alertResult;
+ const {
+ metric,
+ currentValue,
+ threshold,
+ comparator,
+ warningThreshold,
+ warningComparator,
+ } = alertResult;
const noDataValue = i18n.translate(
'xpack.infra.metrics.alerting.threshold.noDataFormattedValue',
{
@@ -167,12 +196,17 @@ const formatAlertResult = (
currentValue: currentValue ?? noDataValue,
};
const formatter = createFormatter('percent');
+ const thresholdToFormat = useWarningThreshold ? warningThreshold! : threshold;
+ const comparatorToFormat = useWarningThreshold ? warningComparator! : comparator;
return {
...alertResult,
currentValue:
currentValue !== null && typeof currentValue !== 'undefined'
? formatter(currentValue)
: noDataValue,
- threshold: Array.isArray(threshold) ? threshold.map((v: number) => formatter(v)) : threshold,
+ threshold: Array.isArray(thresholdToFormat)
+ ? thresholdToFormat.map((v: number) => formatter(v))
+ : thresholdToFormat,
+ comparator: comparatorToFormat,
};
};
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts
index 8576fd7b59299..1adca25504b1f 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts
@@ -20,10 +20,10 @@ describe('Previewing the metric threshold alert type', () => {
alertThrottle: '1m',
alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
- expect(firedResults).toBe(30);
- expect(noDataResults).toBe(0);
- expect(errorResults).toBe(0);
+ const { fired, noData, error, notifications } = ungroupedResult;
+ expect(fired).toBe(30);
+ expect(noData).toBe(0);
+ expect(error).toBe(0);
expect(notifications).toBe(30);
});
@@ -35,10 +35,10 @@ describe('Previewing the metric threshold alert type', () => {
alertThrottle: '3m',
alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
- expect(firedResults).toBe(10);
- expect(noDataResults).toBe(0);
- expect(errorResults).toBe(0);
+ const { fired, noData, error, notifications } = ungroupedResult;
+ expect(fired).toBe(10);
+ expect(noData).toBe(0);
+ expect(error).toBe(0);
expect(notifications).toBe(10);
});
test('returns the expected results using a bucket interval longer than the alert interval', async () => {
@@ -49,10 +49,10 @@ describe('Previewing the metric threshold alert type', () => {
alertThrottle: '30s',
alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
- expect(firedResults).toBe(60);
- expect(noDataResults).toBe(0);
- expect(errorResults).toBe(0);
+ const { fired, noData, error, notifications } = ungroupedResult;
+ expect(fired).toBe(60);
+ expect(noData).toBe(0);
+ expect(error).toBe(0);
expect(notifications).toBe(60);
});
test('returns the expected results using a throttle interval longer than the alert interval', async () => {
@@ -63,10 +63,10 @@ describe('Previewing the metric threshold alert type', () => {
alertThrottle: '3m',
alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
- expect(firedResults).toBe(30);
- expect(noDataResults).toBe(0);
- expect(errorResults).toBe(0);
+ const { fired, noData, error, notifications } = ungroupedResult;
+ expect(fired).toBe(30);
+ expect(noData).toBe(0);
+ expect(error).toBe(0);
expect(notifications).toBe(15);
});
});
@@ -83,15 +83,25 @@ describe('Previewing the metric threshold alert type', () => {
alertThrottle: '1m',
alertOnNoData: true,
});
- const [firedResultsA, noDataResultsA, errorResultsA, notificationsA] = resultA;
- expect(firedResultsA).toBe(30);
- expect(noDataResultsA).toBe(0);
- expect(errorResultsA).toBe(0);
+ const {
+ fired: firedA,
+ noData: noDataA,
+ error: errorA,
+ notifications: notificationsA,
+ } = resultA;
+ expect(firedA).toBe(30);
+ expect(noDataA).toBe(0);
+ expect(errorA).toBe(0);
expect(notificationsA).toBe(30);
- const [firedResultsB, noDataResultsB, errorResultsB, notificationsB] = resultB;
- expect(firedResultsB).toBe(60);
- expect(noDataResultsB).toBe(0);
- expect(errorResultsB).toBe(0);
+ const {
+ fired: firedB,
+ noData: noDataB,
+ error: errorB,
+ notifications: notificationsB,
+ } = resultB;
+ expect(firedB).toBe(60);
+ expect(noDataB).toBe(0);
+ expect(errorB).toBe(0);
expect(notificationsB).toBe(60);
});
});
@@ -113,10 +123,10 @@ describe('Previewing the metric threshold alert type', () => {
alertThrottle: '1m',
alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
- expect(firedResults).toBe(25);
- expect(noDataResults).toBe(10);
- expect(errorResults).toBe(0);
+ const { fired, noData, error, notifications } = ungroupedResult;
+ expect(fired).toBe(25);
+ expect(noData).toBe(10);
+ expect(error).toBe(0);
expect(notifications).toBe(35);
});
});
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
index ac6372a94b1fe..b9fa6659d5fcd 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
@@ -14,6 +14,7 @@ import {
import { ILegacyScopedClusterClient } from '../../../../../../../src/core/server';
import { InfraSource } from '../../../../common/http_api/source_api';
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
+import { PreviewResult } from '../common/types';
import { MetricExpressionParams } from './types';
import { evaluateAlert } from './lib/evaluate_alert';
@@ -39,7 +40,7 @@ export const previewMetricThresholdAlert: (
params: PreviewMetricThresholdAlertParams,
iterations?: number,
precalculatedNumberOfGroups?: number
-) => Promise = async (
+) => Promise = async (
{
callCluster,
params,
@@ -98,6 +99,7 @@ export const previewMetricThresholdAlert: (
numberOfResultBuckets / alertResultsPerExecution
);
let numberOfTimesFired = 0;
+ let numberOfTimesWarned = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
let numberOfNotifications = 0;
@@ -111,6 +113,9 @@ export const previewMetricThresholdAlert: (
const allConditionsFiredInMappedBucket = alertResults.every(
(alertResult) => alertResult[group].shouldFire[mappedBucketIndex]
);
+ const allConditionsWarnInMappedBucket =
+ !allConditionsFiredInMappedBucket &&
+ alertResults.every((alertResult) => alertResult[group].shouldWarn[mappedBucketIndex]);
const someConditionsNoDataInMappedBucket = alertResults.some((alertResult) => {
const hasNoData = alertResult[group].isNoData as boolean[];
return hasNoData[mappedBucketIndex];
@@ -131,6 +136,9 @@ export const previewMetricThresholdAlert: (
} else if (allConditionsFiredInMappedBucket) {
numberOfTimesFired++;
notifyWithThrottle();
+ } else if (allConditionsWarnInMappedBucket) {
+ numberOfTimesWarned++;
+ notifyWithThrottle();
} else if (throttleTracker > 0) {
throttleTracker += alertIntervalInSeconds;
}
@@ -138,7 +146,13 @@ export const previewMetricThresholdAlert: (
throttleTracker = 0;
}
}
- return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications];
+ return {
+ fired: numberOfTimesFired,
+ warning: numberOfTimesWarned,
+ noData: numberOfNoDataResults,
+ error: numberOfErrors,
+ notifications: numberOfNotifications,
+ };
})
);
return previewResults;
@@ -199,7 +213,12 @@ export const previewMetricThresholdAlert: (
.reduce((a, b) => {
if (!a) return b;
if (!b) return a;
- return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
+ const res = { ...a };
+ const entries = (Object.entries(b) as unknown) as Array<[keyof PreviewResult, number]>;
+ for (const [key, value] of entries) {
+ res[key] += value;
+ }
+ return res;
})
);
return zippedResult;
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
index 6d8790f4f430c..e5e3a7bff329e 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
@@ -15,7 +15,11 @@ import {
ActionGroupIdsOf,
} from '../../../../../alerts/server';
import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer';
-import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor';
+import {
+ createMetricThresholdExecutor,
+ FIRED_ACTIONS,
+ WARNING_ACTIONS,
+} from './metric_threshold_executor';
import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types';
import { InfraBackendLibs } from '../../infra_types';
import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils';
@@ -37,7 +41,7 @@ export type MetricThresholdAlertType = AlertType<
Record,
AlertInstanceState,
AlertInstanceContext,
- ActionGroupIdsOf
+ ActionGroupIdsOf
>;
export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions<
/**
@@ -47,7 +51,7 @@ export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions<
Record,
AlertInstanceState,
AlertInstanceContext,
- ActionGroupIdsOf
+ ActionGroupIdsOf
>;
export function registerMetricThresholdAlertType(libs: InfraBackendLibs): MetricThresholdAlertType {
@@ -56,6 +60,8 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs): Metric
comparator: oneOfLiterals(Object.values(Comparator)),
timeUnit: schema.string(),
timeSize: schema.number(),
+ warningThreshold: schema.maybe(schema.arrayOf(schema.number())),
+ warningComparator: schema.maybe(oneOfLiterals(Object.values(Comparator))),
};
const nonCountCriterion = schema.object({
@@ -92,7 +98,7 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs): Metric
),
},
defaultActionGroupId: FIRED_ACTIONS.id,
- actionGroups: [FIRED_ACTIONS],
+ actionGroups: [FIRED_ACTIONS, WARNING_ACTIONS],
minimumLicenseRequired: 'basic',
executor: createMetricThresholdExecutor(libs),
actionVariables: {
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts
index f876e40d9cd1f..37f21022f183d 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts
@@ -29,6 +29,8 @@ interface BaseMetricExpressionParams {
sourceId?: string;
threshold: number[];
comparator: Comparator;
+ warningComparator?: Comparator;
+ warningThreshold?: number[];
}
interface NonCountMetricExpressionParams extends BaseMetricExpressionParams {
diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts
index 0b4df6805759e..11fbe269b854d 100644
--- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts
@@ -8,13 +8,21 @@
import { PluginSetupContract } from '../../../../alerts/server';
import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type';
import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type';
+import { registerMetricAnomalyAlertType } from './metric_anomaly/register_metric_anomaly_alert_type';
+
import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type';
import { InfraBackendLibs } from '../infra_types';
+import { MlPluginSetup } from '../../../../ml/server';
-const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => {
+const registerAlertTypes = (
+ alertingPlugin: PluginSetupContract,
+ libs: InfraBackendLibs,
+ ml?: MlPluginSetup
+) => {
if (alertingPlugin) {
alertingPlugin.registerType(registerMetricThresholdAlertType(libs));
alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs));
+ alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml));
const registerFns = [registerLogThresholdAlertType];
registerFns.forEach((fn) => {
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/common.ts
index 0182cb0e4099a..686f27d714cc1 100644
--- a/x-pack/plugins/infra/server/lib/infra_ml/common.ts
+++ b/x-pack/plugins/infra/server/lib/infra_ml/common.ts
@@ -17,6 +17,23 @@ import {
import { decodeOrThrow } from '../../../common/runtime_types';
import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing';
+export interface MappedAnomalyHit {
+ id: string;
+ anomalyScore: number;
+ typical: number;
+ actual: number;
+ jobId: string;
+ startTime: number;
+ duration: number;
+ influencers: string[];
+ categoryId?: string;
+}
+
+export interface InfluencerFilter {
+ fieldName: string;
+ fieldValue: string;
+}
+
export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) {
const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
const {
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/index.ts
index d346b71d76aa8..82093b1a359d0 100644
--- a/x-pack/plugins/infra/server/lib/infra_ml/index.ts
+++ b/x-pack/plugins/infra/server/lib/infra_ml/index.ts
@@ -8,3 +8,4 @@
export * from './errors';
export * from './metrics_hosts_anomalies';
export * from './metrics_k8s_anomalies';
+export { MappedAnomalyHit } from './common';
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts
index 072f07dfaffdb..f6e11f5294191 100644
--- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts
+++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts
@@ -5,11 +5,10 @@
* 2.0.
*/
-import type { InfraPluginRequestHandlerContext } from '../../types';
import { InfraRequestHandlerContext } from '../../types';
import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
-import { fetchMlJob } from './common';
-import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml';
+import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common';
+import { getJobId, metricsHostsJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml';
import { Sort, Pagination } from '../../../common/http_api/infra_ml';
import type { MlSystem, MlAnomalyDetectors } from '../../types';
import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
@@ -19,18 +18,6 @@ import {
createMetricsHostsAnomaliesQuery,
} from './queries/metrics_hosts_anomalies';
-interface MappedAnomalyHit {
- id: string;
- anomalyScore: number;
- typical: number;
- actual: number;
- jobId: string;
- startTime: number;
- duration: number;
- influencers: string[];
- categoryId?: string;
-}
-
async function getCompatibleAnomaliesJobIds(
spaceId: string,
sourceId: string,
@@ -74,13 +61,15 @@ async function getCompatibleAnomaliesJobIds(
}
export async function getMetricsHostsAnomalies(
- context: InfraPluginRequestHandlerContext & { infra: Required },
+ context: Required,
sourceId: string,
+ anomalyThreshold: ANOMALY_THRESHOLD,
startTime: number,
endTime: number,
metric: 'memory_usage' | 'network_in' | 'network_out' | undefined,
sort: Sort,
- pagination: Pagination
+ pagination: Pagination,
+ influencerFilter?: InfluencerFilter
) {
const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies');
@@ -88,10 +77,10 @@ export async function getMetricsHostsAnomalies(
jobIds,
timing: { spans: jobSpans },
} = await getCompatibleAnomaliesJobIds(
- context.infra.spaceId,
+ context.spaceId,
sourceId,
metric,
- context.infra.mlAnomalyDetectors
+ context.mlAnomalyDetectors
);
if (jobIds.length === 0) {
@@ -107,12 +96,14 @@ export async function getMetricsHostsAnomalies(
hasMoreEntries,
timing: { spans: fetchLogEntryAnomaliesSpans },
} = await fetchMetricsHostsAnomalies(
- context.infra.mlSystem,
+ context.mlSystem,
+ anomalyThreshold,
jobIds,
startTime,
endTime,
sort,
- pagination
+ pagination,
+ influencerFilter
);
const data = anomalies.map((anomaly) => {
@@ -162,11 +153,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
async function fetchMetricsHostsAnomalies(
mlSystem: MlSystem,
+ anomalyThreshold: ANOMALY_THRESHOLD,
jobIds: string[],
startTime: number,
endTime: number,
sort: Sort,
- pagination: Pagination
+ pagination: Pagination,
+ influencerFilter?: InfluencerFilter
) {
// We'll request 1 extra entry on top of our pageSize to determine if there are
// more entries to be fetched. This avoids scenarios where the client side can't
@@ -178,7 +171,15 @@ async function fetchMetricsHostsAnomalies(
const results = decodeOrThrow(metricsHostsAnomaliesResponseRT)(
await mlSystem.mlAnomalySearch(
- createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination),
+ createMetricsHostsAnomaliesQuery({
+ jobIds,
+ anomalyThreshold,
+ startTime,
+ endTime,
+ sort,
+ pagination: expandedPagination,
+ influencerFilter,
+ }),
jobIds
)
);
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts
index 44837d88ddb43..34039e9107f00 100644
--- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts
+++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts
@@ -5,11 +5,10 @@
* 2.0.
*/
-import type { InfraPluginRequestHandlerContext } from '../../types';
import { InfraRequestHandlerContext } from '../../types';
import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
-import { fetchMlJob } from './common';
-import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml';
+import { fetchMlJob, MappedAnomalyHit, InfluencerFilter } from './common';
+import { getJobId, metricsK8SJobTypes, ANOMALY_THRESHOLD } from '../../../common/infra_ml';
import { Sort, Pagination } from '../../../common/http_api/infra_ml';
import type { MlSystem, MlAnomalyDetectors } from '../../types';
import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
@@ -19,18 +18,6 @@ import {
createMetricsK8sAnomaliesQuery,
} from './queries/metrics_k8s_anomalies';
-interface MappedAnomalyHit {
- id: string;
- anomalyScore: number;
- typical: number;
- actual: number;
- jobId: string;
- startTime: number;
- influencers: string[];
- duration: number;
- categoryId?: string;
-}
-
async function getCompatibleAnomaliesJobIds(
spaceId: string,
sourceId: string,
@@ -74,13 +61,15 @@ async function getCompatibleAnomaliesJobIds(
}
export async function getMetricK8sAnomalies(
- context: InfraPluginRequestHandlerContext & { infra: Required },
+ context: Required,
sourceId: string,
+ anomalyThreshold: ANOMALY_THRESHOLD,
startTime: number,
endTime: number,
metric: 'memory_usage' | 'network_in' | 'network_out' | undefined,
sort: Sort,
- pagination: Pagination
+ pagination: Pagination,
+ influencerFilter?: InfluencerFilter
) {
const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies');
@@ -88,10 +77,10 @@ export async function getMetricK8sAnomalies(
jobIds,
timing: { spans: jobSpans },
} = await getCompatibleAnomaliesJobIds(
- context.infra.spaceId,
+ context.spaceId,
sourceId,
metric,
- context.infra.mlAnomalyDetectors
+ context.mlAnomalyDetectors
);
if (jobIds.length === 0) {
@@ -106,12 +95,14 @@ export async function getMetricK8sAnomalies(
hasMoreEntries,
timing: { spans: fetchLogEntryAnomaliesSpans },
} = await fetchMetricK8sAnomalies(
- context.infra.mlSystem,
+ context.mlSystem,
+ anomalyThreshold,
jobIds,
startTime,
endTime,
sort,
- pagination
+ pagination,
+ influencerFilter
);
const data = anomalies.map((anomaly) => {
@@ -158,11 +149,13 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
async function fetchMetricK8sAnomalies(
mlSystem: MlSystem,
+ anomalyThreshold: ANOMALY_THRESHOLD,
jobIds: string[],
startTime: number,
endTime: number,
sort: Sort,
- pagination: Pagination
+ pagination: Pagination,
+ influencerFilter?: InfluencerFilter | undefined
) {
// We'll request 1 extra entry on top of our pageSize to determine if there are
// more entries to be fetched. This avoids scenarios where the client side can't
@@ -174,7 +167,15 @@ async function fetchMetricK8sAnomalies(
const results = decodeOrThrow(metricsK8sAnomaliesResponseRT)(
await mlSystem.mlAnomalySearch(
- createMetricsK8sAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination),
+ createMetricsK8sAnomaliesQuery({
+ jobIds,
+ anomalyThreshold,
+ startTime,
+ endTime,
+ sort,
+ pagination: expandedPagination,
+ influencerFilter,
+ }),
jobIds
)
);
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts
index b3676fc54aeaa..6f996a672a44a 100644
--- a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts
@@ -77,3 +77,35 @@ export const createDatasetsFilters = (datasets?: string[]) =>
},
]
: [];
+
+export const createInfluencerFilter = ({
+ fieldName,
+ fieldValue,
+}: {
+ fieldName: string;
+ fieldValue: string;
+}) => [
+ {
+ nested: {
+ path: 'influencers',
+ query: {
+ bool: {
+ must: [
+ {
+ match: {
+ 'influencers.influencer_field_name': fieldName,
+ },
+ },
+ {
+ query_string: {
+ fields: ['influencers.influencer_field_values'],
+ query: fieldValue,
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+];
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts
new file mode 100644
index 0000000000000..4c3e0ca8bc26f
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { createMetricsHostsAnomaliesQuery } from './metrics_hosts_anomalies';
+import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
+
+describe('createMetricsHostAnomaliesQuery', () => {
+ const jobIds = ['kibana-metrics-ui-default-default-hosts_memory_usage'];
+ const anomalyThreshold = 30;
+ const startTime = 1612454527112;
+ const endTime = 1612541227112;
+ const sort: Sort = { field: 'anomalyScore', direction: 'desc' };
+ const pagination: Pagination = { pageSize: 101 };
+
+ test('returns the correct query', () => {
+ expect(
+ createMetricsHostsAnomaliesQuery({
+ jobIds,
+ anomalyThreshold,
+ startTime,
+ endTime,
+ sort,
+ pagination,
+ })
+ ).toMatchObject({
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ trackScores: false,
+ trackTotalHits: false,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ { terms: { job_id: ['kibana-metrics-ui-default-default-hosts_memory_usage'] } },
+ { range: { record_score: { gte: 30 } } },
+ { range: { timestamp: { gte: 1612454527112, lte: 1612541227112 } } },
+ { terms: { result_type: ['record'] } },
+ ],
+ },
+ },
+ sort: [{ record_score: 'desc' }, { _doc: 'desc' }],
+ size: 101,
+ _source: [
+ 'job_id',
+ 'record_score',
+ 'typical',
+ 'actual',
+ 'partition_field_value',
+ 'timestamp',
+ 'bucket_span',
+ 'by_field_value',
+ 'host.name',
+ 'influencers.influencer_field_name',
+ 'influencers.influencer_field_values',
+ ],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts
index 07b25931d838e..7808851508a7c 100644
--- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts
@@ -6,6 +6,7 @@
*/
import * as rt from 'io-ts';
+import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
import {
createJobIdsFilters,
@@ -13,7 +14,9 @@ import {
createResultTypeFilters,
defaultRequestParameters,
createAnomalyScoreFilter,
+ createInfluencerFilter,
} from './common';
+import { InfluencerFilter } from '../common';
import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
// TODO: Reassess validity of this against ML docs
@@ -25,23 +28,37 @@ const sortToMlFieldMap = {
startTime: 'timestamp',
};
-export const createMetricsHostsAnomaliesQuery = (
- jobIds: string[],
- startTime: number,
- endTime: number,
- sort: Sort,
- pagination: Pagination
-) => {
+export const createMetricsHostsAnomaliesQuery = ({
+ jobIds,
+ anomalyThreshold,
+ startTime,
+ endTime,
+ sort,
+ pagination,
+ influencerFilter,
+}: {
+ jobIds: string[];
+ anomalyThreshold: ANOMALY_THRESHOLD;
+ startTime: number;
+ endTime: number;
+ sort: Sort;
+ pagination: Pagination;
+ influencerFilter?: InfluencerFilter;
+}) => {
const { field } = sort;
const { pageSize } = pagination;
const filters = [
...createJobIdsFilters(jobIds),
- ...createAnomalyScoreFilter(50),
+ ...createAnomalyScoreFilter(anomalyThreshold),
...createTimeRangeFilters(startTime, endTime),
...createResultTypeFilters(['record']),
];
+ const influencerQuery = influencerFilter
+ ? { must: createInfluencerFilter(influencerFilter) }
+ : {};
+
const sourceFields = [
'job_id',
'record_score',
@@ -69,6 +86,7 @@ export const createMetricsHostsAnomaliesQuery = (
query: {
bool: {
filter: filters,
+ ...influencerQuery,
},
},
search_after: queryCursor,
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts
new file mode 100644
index 0000000000000..81dcb390dff56
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { createMetricsK8sAnomaliesQuery } from './metrics_k8s_anomalies';
+import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
+
+describe('createMetricsK8sAnomaliesQuery', () => {
+ const jobIds = ['kibana-metrics-ui-default-default-k8s_memory_usage'];
+ const anomalyThreshold = 30;
+ const startTime = 1612454527112;
+ const endTime = 1612541227112;
+ const sort: Sort = { field: 'anomalyScore', direction: 'desc' };
+ const pagination: Pagination = { pageSize: 101 };
+
+ test('returns the correct query', () => {
+ expect(
+ createMetricsK8sAnomaliesQuery({
+ jobIds,
+ anomalyThreshold,
+ startTime,
+ endTime,
+ sort,
+ pagination,
+ })
+ ).toMatchObject({
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ trackScores: false,
+ trackTotalHits: false,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ { terms: { job_id: ['kibana-metrics-ui-default-default-k8s_memory_usage'] } },
+ { range: { record_score: { gte: 30 } } },
+ { range: { timestamp: { gte: 1612454527112, lte: 1612541227112 } } },
+ { terms: { result_type: ['record'] } },
+ ],
+ },
+ },
+ sort: [{ record_score: 'desc' }, { _doc: 'desc' }],
+ size: 101,
+ _source: [
+ 'job_id',
+ 'record_score',
+ 'typical',
+ 'actual',
+ 'partition_field_value',
+ 'timestamp',
+ 'bucket_span',
+ 'by_field_value',
+ 'influencers.influencer_field_name',
+ 'influencers.influencer_field_values',
+ ],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts
index 8a6e9396fb098..54eea067177ed 100644
--- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts
@@ -6,6 +6,7 @@
*/
import * as rt from 'io-ts';
+import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml';
import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
import {
createJobIdsFilters,
@@ -13,7 +14,9 @@ import {
createResultTypeFilters,
defaultRequestParameters,
createAnomalyScoreFilter,
+ createInfluencerFilter,
} from './common';
+import { InfluencerFilter } from '../common';
import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
// TODO: Reassess validity of this against ML docs
@@ -25,23 +28,37 @@ const sortToMlFieldMap = {
startTime: 'timestamp',
};
-export const createMetricsK8sAnomaliesQuery = (
- jobIds: string[],
- startTime: number,
- endTime: number,
- sort: Sort,
- pagination: Pagination
-) => {
+export const createMetricsK8sAnomaliesQuery = ({
+ jobIds,
+ anomalyThreshold,
+ startTime,
+ endTime,
+ sort,
+ pagination,
+ influencerFilter,
+}: {
+ jobIds: string[];
+ anomalyThreshold: ANOMALY_THRESHOLD;
+ startTime: number;
+ endTime: number;
+ sort: Sort;
+ pagination: Pagination;
+ influencerFilter?: InfluencerFilter;
+}) => {
const { field } = sort;
const { pageSize } = pagination;
const filters = [
...createJobIdsFilters(jobIds),
- ...createAnomalyScoreFilter(50),
+ ...createAnomalyScoreFilter(anomalyThreshold),
...createTimeRangeFilters(startTime, endTime),
...createResultTypeFilters(['record']),
];
+ const influencerQuery = influencerFilter
+ ? { must: createInfluencerFilter(influencerFilter) }
+ : {};
+
const sourceFields = [
'job_id',
'record_score',
@@ -68,6 +85,7 @@ export const createMetricsK8sAnomaliesQuery = (
query: {
bool: {
filter: filters,
+ ...influencerQuery,
},
},
search_after: queryCursor,
diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts
index 3fc098bcf8846..f5465a967f2a5 100644
--- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts
+++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts
@@ -281,7 +281,6 @@ async function fetchLogEntryAnomalies(
nextPageCursor: hits[hits.length - 1].sort,
}
: undefined;
-
const anomalies = hits.map((result) => {
const {
// eslint-disable-next-line @typescript-eslint/naming-convention
diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts
index ce7c4410baca9..1b924619a905c 100644
--- a/x-pack/plugins/infra/server/lib/sources/defaults.ts
+++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts
@@ -45,4 +45,5 @@ export const defaultSourceConfiguration: InfraSourceConfiguration = {
},
},
],
+ anomalyThreshold: 50,
};
diff --git a/x-pack/plugins/infra/server/lib/sources/errors.ts b/x-pack/plugins/infra/server/lib/sources/errors.ts
index fb0dc3b031511..082dfc611cc5b 100644
--- a/x-pack/plugins/infra/server/lib/sources/errors.ts
+++ b/x-pack/plugins/infra/server/lib/sources/errors.ts
@@ -4,10 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+/* eslint-disable max-classes-per-file */
export class NotFoundError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
+
+export class AnomalyThresholdRangeError extends Error {
+ constructor(message?: string) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts
index 4cea6cbe32cfb..21b7643ca6a7f 100644
--- a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts
+++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts
@@ -126,6 +126,7 @@ const createTestSourceConfiguration = (logAlias: string, metricAlias: string) =>
],
logAlias,
metricAlias,
+ anomalyThreshold: 20,
},
id: 'TEST_ID',
type: infraSourceConfigurationSavedObjectName,
diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts
index aad877a077acf..fe005b04978da 100644
--- a/x-pack/plugins/infra/server/lib/sources/sources.ts
+++ b/x-pack/plugins/infra/server/lib/sources/sources.ts
@@ -10,9 +10,10 @@ import { failure } from 'io-ts/lib/PathReporter';
import { identity, constant } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
+import { inRange } from 'lodash';
import { SavedObjectsClientContract } from 'src/core/server';
import { defaultSourceConfiguration } from './defaults';
-import { NotFoundError } from './errors';
+import { AnomalyThresholdRangeError, NotFoundError } from './errors';
import { infraSourceConfigurationSavedObjectName } from './saved_object_type';
import {
InfraSavedSourceConfiguration,
@@ -104,6 +105,9 @@ export class InfraSources {
source: InfraSavedSourceConfiguration
) {
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
+ const { anomalyThreshold } = source;
+ if (anomalyThreshold && !inRange(anomalyThreshold, 0, 101))
+ throw new AnomalyThresholdRangeError('anomalyThreshold must be 1-100');
const newSourceConfiguration = mergeSourceConfiguration(
staticDefaultSourceConfiguration,
@@ -140,6 +144,10 @@ export class InfraSources {
sourceProperties: InfraSavedSourceConfiguration
) {
const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration();
+ const { anomalyThreshold } = sourceProperties;
+
+ if (anomalyThreshold && !inRange(anomalyThreshold, 0, 101))
+ throw new AnomalyThresholdRangeError('anomalyThreshold must be 1-100');
const { configuration, version } = await this.getSourceConfiguration(
savedObjectsClient,
diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts
index 99555fa56acd5..0ac49e05b36b9 100644
--- a/x-pack/plugins/infra/server/plugin.ts
+++ b/x-pack/plugins/infra/server/plugin.ts
@@ -137,7 +137,7 @@ export class InfraServerPlugin implements Plugin {
]);
initInfraServer(this.libs);
- registerAlertTypes(plugins.alerts, this.libs);
+ registerAlertTypes(plugins.alerts, this.libs, plugins.ml);
core.http.registerRouteHandlerContext(
'infra',
diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts
index ba16221108958..3da560135eaf4 100644
--- a/x-pack/plugins/infra/server/routes/alerting/preview.ts
+++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts
@@ -5,20 +5,25 @@
* 2.0.
*/
+import { PreviewResult } from '../../lib/alerting/common/types';
import {
METRIC_THRESHOLD_ALERT_TYPE_ID,
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
+ METRIC_ANOMALY_ALERT_TYPE_ID,
INFRA_ALERT_PREVIEW_PATH,
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
alertPreviewRequestParamsRT,
alertPreviewSuccessResponsePayloadRT,
MetricThresholdAlertPreviewRequestParams,
InventoryAlertPreviewRequestParams,
+ MetricAnomalyAlertPreviewRequestParams,
} from '../../../common/alerting/metrics';
import { createValidationFunction } from '../../../common/runtime_types';
import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert';
import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert';
+import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/preview_metric_anomaly_alert';
import { InfraBackendLibs } from '../../lib/infra_types';
+import { assertHasInfraMlPlugins } from '../../utils/request_context';
export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => {
const { callWithRequest } = framework;
@@ -32,8 +37,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
},
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
const {
- criteria,
- filterQuery,
lookback,
sourceId,
alertType,
@@ -54,7 +57,11 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
try {
switch (alertType) {
case METRIC_THRESHOLD_ALERT_TYPE_ID: {
- const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams;
+ const {
+ groupBy,
+ criteria,
+ filterQuery,
+ } = request.body as MetricThresholdAlertPreviewRequestParams;
const previewResult = await previewMetricThresholdAlert({
callCluster,
params: { criteria, filterQuery, groupBy },
@@ -65,33 +72,17 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
alertOnNoData,
});
- const numberOfGroups = previewResult.length;
- const resultTotals = previewResult.reduce(
- (totals, [firedResult, noDataResult, errorResult, notifications]) => {
- return {
- ...totals,
- fired: totals.fired + firedResult,
- noData: totals.noData + noDataResult,
- error: totals.error + errorResult,
- notifications: totals.notifications + notifications,
- };
- },
- {
- fired: 0,
- noData: 0,
- error: 0,
- notifications: 0,
- }
- );
+ const payload = processPreviewResults(previewResult);
return response.ok({
- body: alertPreviewSuccessResponsePayloadRT.encode({
- numberOfGroups,
- resultTotals,
- }),
+ body: alertPreviewSuccessResponsePayloadRT.encode(payload),
});
}
case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: {
- const { nodeType } = request.body as InventoryAlertPreviewRequestParams;
+ const {
+ nodeType,
+ criteria,
+ filterQuery,
+ } = request.body as InventoryAlertPreviewRequestParams;
const previewResult = await previewInventoryMetricThresholdAlert({
callCluster,
params: { criteria, filterQuery, nodeType },
@@ -102,29 +93,42 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
alertOnNoData,
});
- const numberOfGroups = previewResult.length;
- const resultTotals = previewResult.reduce(
- (totals, [firedResult, noDataResult, errorResult, notifications]) => {
- return {
- ...totals,
- fired: totals.fired + firedResult,
- noData: totals.noData + noDataResult,
- error: totals.error + errorResult,
- notifications: totals.notifications + notifications,
- };
- },
- {
- fired: 0,
- noData: 0,
- error: 0,
- notifications: 0,
- }
- );
+ const payload = processPreviewResults(previewResult);
+
+ return response.ok({
+ body: alertPreviewSuccessResponsePayloadRT.encode(payload),
+ });
+ }
+ case METRIC_ANOMALY_ALERT_TYPE_ID: {
+ assertHasInfraMlPlugins(requestContext);
+ const {
+ nodeType,
+ metric,
+ threshold,
+ influencerFilter,
+ } = request.body as MetricAnomalyAlertPreviewRequestParams;
+ const { mlAnomalyDetectors, mlSystem, spaceId } = requestContext.infra;
+
+ const previewResult = await previewMetricAnomalyAlert({
+ mlAnomalyDetectors,
+ mlSystem,
+ spaceId,
+ params: { nodeType, metric, threshold, influencerFilter },
+ lookback,
+ sourceId: source.id,
+ alertInterval,
+ alertThrottle,
+ alertOnNoData,
+ });
return response.ok({
body: alertPreviewSuccessResponsePayloadRT.encode({
- numberOfGroups,
- resultTotals,
+ numberOfGroups: 1,
+ resultTotals: {
+ ...previewResult,
+ error: 0,
+ noData: 0,
+ },
}),
});
}
@@ -150,3 +154,27 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
})
);
};
+
+const processPreviewResults = (previewResult: PreviewResult[]) => {
+ const numberOfGroups = previewResult.length;
+ const resultTotals = previewResult.reduce(
+ (totals, { fired, warning, noData, error, notifications }) => {
+ return {
+ ...totals,
+ fired: totals.fired + fired,
+ warning: totals.warning + warning,
+ noData: totals.noData + noData,
+ error: totals.error + error,
+ notifications: totals.notifications + notifications,
+ };
+ },
+ {
+ fired: 0,
+ warning: 0,
+ noData: 0,
+ error: 0,
+ notifications: 0,
+ }
+ );
+ return { numberOfGroups, resultTotals };
+};
diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts
index 215ebf3280c03..6e227cfc12d11 100644
--- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts
+++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts
@@ -34,6 +34,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
const {
data: {
sourceId,
+ anomalyThreshold,
timeRange: { startTime, endTime },
sort: sortParam,
pagination: paginationParam,
@@ -52,8 +53,9 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
hasMoreEntries,
timing,
} = await getMetricsHostsAnomalies(
- requestContext,
+ requestContext.infra,
sourceId,
+ anomalyThreshold,
startTime,
endTime,
metric,
diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts
index 906278be657d3..1c2c4947a02ea 100644
--- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts
+++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts
@@ -33,6 +33,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
const {
data: {
sourceId,
+ anomalyThreshold,
timeRange: { startTime, endTime },
sort: sortParam,
pagination: paginationParam,
@@ -51,8 +52,9 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
hasMoreEntries,
timing,
} = await getMetricK8sAnomalies(
- requestContext,
+ requestContext.infra,
sourceId,
+ anomalyThreshold,
startTime,
endTime,
metric,
diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts
index d50495689e9d8..23c2ce5f0c21f 100644
--- a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts
+++ b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts
@@ -9,7 +9,6 @@ export * from './log_entry_categories';
export * from './log_entry_category_datasets';
export * from './log_entry_category_datasets_stats';
export * from './log_entry_category_examples';
-export * from './log_entry_rate';
export * from './log_entry_examples';
export * from './log_entry_anomalies';
export * from './log_entry_anomalies_datasets';
diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts
deleted file mode 100644
index c1762f88a6cdd..0000000000000
--- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts
+++ /dev/null
@@ -1,90 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import Boom from '@hapi/boom';
-import { InfraBackendLibs } from '../../../lib/infra_types';
-import {
- LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH,
- getLogEntryRateRequestPayloadRT,
- getLogEntryRateSuccessReponsePayloadRT,
- GetLogEntryRateSuccessResponsePayload,
-} from '../../../../common/http_api/log_analysis';
-import { createValidationFunction } from '../../../../common/runtime_types';
-import { getLogEntryRateBuckets } from '../../../lib/log_analysis';
-import { assertHasInfraMlPlugins } from '../../../utils/request_context';
-import { isMlPrivilegesError } from '../../../lib/log_analysis/errors';
-
-export const initGetLogEntryRateRoute = ({ framework }: InfraBackendLibs) => {
- framework.registerRoute(
- {
- method: 'post',
- path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH,
- validate: {
- body: createValidationFunction(getLogEntryRateRequestPayloadRT),
- },
- },
- framework.router.handleLegacyErrors(async (requestContext, request, response) => {
- const {
- data: { sourceId, timeRange, bucketDuration, datasets },
- } = request.body;
-
- try {
- assertHasInfraMlPlugins(requestContext);
-
- const logEntryRateBuckets = await getLogEntryRateBuckets(
- requestContext,
- sourceId,
- timeRange.startTime,
- timeRange.endTime,
- bucketDuration,
- datasets
- );
-
- return response.ok({
- body: getLogEntryRateSuccessReponsePayloadRT.encode({
- data: {
- bucketDuration,
- histogramBuckets: logEntryRateBuckets,
- totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets),
- },
- }),
- });
- } catch (error) {
- if (Boom.isBoom(error)) {
- throw error;
- }
-
- if (isMlPrivilegesError(error)) {
- return response.customError({
- statusCode: 403,
- body: {
- message: error.message,
- },
- });
- }
-
- return response.customError({
- statusCode: error.statusCode ?? 500,
- body: {
- message: error.message ?? 'An unexpected error occurred',
- },
- });
- }
- })
- );
-};
-
-const getTotalNumberOfLogEntries = (
- logEntryRateBuckets: GetLogEntryRateSuccessResponsePayload['data']['histogramBuckets']
-) => {
- return logEntryRateBuckets.reduce((sumNumberOfLogEntries, bucket) => {
- const sumPartitions = bucket.partitions.reduce((partitionsTotal, partition) => {
- return (partitionsTotal += partition.numberOfLogEntries);
- }, 0);
- return (sumNumberOfLogEntries += sumPartitions);
- }, 0);
-};
diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts
index f1132049bd03c..5c3827e56ce79 100644
--- a/x-pack/plugins/infra/server/routes/source/index.ts
+++ b/x-pack/plugins/infra/server/routes/source/index.ts
@@ -16,6 +16,7 @@ import {
import { InfraBackendLibs } from '../../lib/infra_types';
import { hasData } from '../../lib/sources/has_data';
import { createSearchClient } from '../../lib/create_search_client';
+import { AnomalyThresholdRangeError } from '../../lib/sources/errors';
const typeToInfraIndexType = (value: string | undefined) => {
switch (value) {
@@ -137,6 +138,15 @@ export const initSourceRoute = (libs: InfraBackendLibs) => {
throw error;
}
+ if (error instanceof AnomalyThresholdRangeError) {
+ return response.customError({
+ statusCode: 400,
+ body: {
+ message: error.message,
+ },
+ });
+ }
+
return response.customError({
statusCode: error.statusCode ?? 500,
body: {
diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts
index 16a45dc6489ee..bc4976a068f4d 100644
--- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts
+++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts
@@ -279,6 +279,7 @@ const createSourceConfigurationMock = (): InfraSource => ({
timestamp: 'TIMESTAMP_FIELD',
tiebreaker: 'TIEBREAKER_FIELD',
},
+ anomalyThreshold: 20,
},
});
diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts
index 6bcc61f2be4a6..7ac8b71c04b2a 100644
--- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts
+++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts
@@ -216,6 +216,7 @@ const createSourceConfigurationMock = () => ({
timestamp: 'TIMESTAMP_FIELD',
tiebreaker: 'TIEBREAKER_FIELD',
},
+ anomalyThreshold: 20,
},
});
diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts
index 62de8baa66d5f..7a21599605b52 100644
--- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts
@@ -8,7 +8,11 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { Query } from 'src/plugins/data/public';
-import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types';
+import {
+ HeatmapStyleDescriptor,
+ StyleDescriptor,
+ VectorStyleDescriptor,
+} from './style_property_descriptor_types';
import { DataRequestDescriptor } from './data_request_descriptor_types';
import { AbstractSourceDescriptor, TermJoinSourceDescriptor } from './source_descriptor_types';
@@ -40,3 +44,7 @@ export type LayerDescriptor = {
export type VectorLayerDescriptor = LayerDescriptor & {
style: VectorStyleDescriptor;
};
+
+export type HeatmapLayerDescriptor = LayerDescriptor & {
+ style: HeatmapStyleDescriptor;
+};
diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json
index 744cc18c36f3e..1d4f76db79751 100644
--- a/x-pack/plugins/maps/kibana.json
+++ b/x-pack/plugins/maps/kibana.json
@@ -11,7 +11,7 @@
"features",
"inspector",
"data",
- "mapsFileUpload",
+ "fileUpload",
"uiActions",
"navigation",
"visualizations",
@@ -25,7 +25,8 @@
],
"optionalPlugins": [
"home",
- "savedObjectsTagging"
+ "savedObjectsTagging",
+ "charts"
],
"ui": true,
"server": true,
diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts
index 2e6a8098e5c21..5e8a18348ac5a 100644
--- a/x-pack/plugins/maps/public/actions/data_request_actions.ts
+++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts
@@ -40,7 +40,7 @@ import {
UPDATE_SOURCE_DATA_REQUEST,
} from './map_action_constants';
import { ILayer } from '../classes/layers/layer';
-import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../classes/layers/vector_layer';
import { DataMeta, MapExtent, MapFilters } from '../../common/descriptor_types';
import { DataRequestAbortError } from '../classes/util/data_request';
import { scaleBounds, turfBboxToBounds } from '../../common/elasticsearch_util';
diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts
index 16aa44af4460f..d68e4744975f1 100644
--- a/x-pack/plugins/maps/public/actions/layer_actions.ts
+++ b/x-pack/plugins/maps/public/actions/layer_actions.ts
@@ -42,7 +42,7 @@ import { clearDataRequests, syncDataForLayerId, updateStyleMeta } from './data_r
import { cleanTooltipStateForLayer } from './tooltip_actions';
import { JoinDescriptor, LayerDescriptor, StyleDescriptor } from '../../common/descriptor_types';
import { ILayer } from '../classes/layers/layer';
-import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../classes/layers/vector_layer';
import { LAYER_STYLE_TYPE, LAYER_TYPE } from '../../common/constants';
import { IVectorStyle } from '../classes/styles/vector/vector_style';
import { notifyLicensedFeatureUsage } from '../licensed_features';
diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.test.js b/x-pack/plugins/maps/public/classes/joins/inner_join.test.js
index 749745530af7e..67fbf94fd1787 100644
--- a/x-pack/plugins/maps/public/classes/joins/inner_join.test.js
+++ b/x-pack/plugins/maps/public/classes/joins/inner_join.test.js
@@ -9,7 +9,7 @@ import { InnerJoin } from './inner_join';
import { SOURCE_TYPES } from '../../../common/constants';
jest.mock('../../kibana_services', () => {});
-jest.mock('../layers/vector_layer/vector_layer', () => {});
+jest.mock('../layers/vector_layer', () => {});
const rightSource = {
type: SOURCE_TYPES.ES_TERM_SOURCE,
diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
index efd022292f90b..d3a4fa4101ac9 100644
--- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
+++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts
@@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { VectorLayer } from '../vector_layer/vector_layer';
+import { IVectorLayer, VectorLayer } from '../vector_layer';
import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style';
import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults';
import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property';
@@ -24,7 +24,6 @@ import {
} from '../../../../common/constants';
import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source';
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
-import { IVectorLayer } from '../vector_layer/vector_layer';
import { IESSource } from '../../sources/es_source';
import { ISource } from '../../sources/source';
import { DataRequestContext } from '../../../actions';
@@ -169,6 +168,7 @@ function getClusterStyleDescriptor(
}
export interface BlendedVectorLayerArguments {
+ chartsPaletteServiceGetColor?: (value: string) => string | null;
source: IVectorSource;
layerDescriptor: VectorLayerDescriptor;
}
@@ -205,7 +205,12 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer {
this._documentStyle,
this._clusterSource
);
- this._clusterStyle = new VectorStyle(clusterStyleDescriptor, this._clusterSource, this);
+ this._clusterStyle = new VectorStyle(
+ clusterStyleDescriptor,
+ this._clusterSource,
+ this,
+ options.chartsPaletteServiceGetColor
+ );
let isClustered = false;
const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID);
diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts
index a85ba041c4351..a4955a965d77c 100644
--- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts
+++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts
@@ -23,7 +23,7 @@ import {
ESSearchSourceDescriptor,
} from '../../../../common/descriptor_types';
import { VectorStyle } from '../../styles/vector/vector_style';
-import { VectorLayer } from '../vector_layer/vector_layer';
+import { VectorLayer } from '../vector_layer';
import { EMSFileSource } from '../../sources/ems_file_source';
// @ts-ignore
import { ESSearchSource } from '../../sources/es_search_source';
diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts
index 8e0d234445355..658a093321500 100644
--- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts
+++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts
@@ -22,8 +22,7 @@ import {
} from '../../../common/constants';
import { VectorStyle } from '../styles/vector/vector_style';
import { EMSFileSource } from '../sources/ems_file_source';
-// @ts-ignore
-import { VectorLayer } from './vector_layer/vector_layer';
+import { VectorLayer } from './vector_layer';
import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults';
import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes';
import { getJoinAggKey } from '../../../common/get_agg_key';
diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts
index a9de8c98ee557..e3e5f3878ee56 100644
--- a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts
+++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts
@@ -23,11 +23,9 @@ import {
VECTOR_STYLES,
} from '../../../common/constants';
import { VectorStyle } from '../styles/vector/vector_style';
-// @ts-ignore
import { ESGeoGridSource } from '../sources/es_geo_grid_source';
-import { VectorLayer } from './vector_layer/vector_layer';
-// @ts-ignore
-import { HeatmapLayer } from './heatmap_layer/heatmap_layer';
+import { VectorLayer } from './vector_layer';
+import { HeatmapLayer } from './heatmap_layer';
import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults';
import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes';
import { getSourceAggKey } from '../../../common/get_agg_key';
diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx
index a61ea4ce713a8..44a22f1529f18 100644
--- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx
@@ -16,10 +16,10 @@ import {
} from '../../../../common/constants';
import { getFileUploadComponent } from '../../../kibana_services';
import { GeoJsonFileSource } from '../../sources/geojson_file_source';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import { VectorLayer } from '../../layers/vector_layer';
import { createDefaultLayerDescriptor } from '../../sources/es_search_source';
import { RenderWizardArguments } from '../../layers/layer_wizard_registry';
-import { FileUploadComponentProps } from '../../../../../maps_file_upload/public';
+import { FileUploadComponentProps } from '../../../../../file_upload/public';
export const INDEX_SETUP_STEP_ID = 'INDEX_SETUP_STEP_ID';
export const INDEXING_STEP_ID = 'INDEXING_STEP_ID';
diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js
deleted file mode 100644
index 97cc7151112bf..0000000000000
--- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js
+++ /dev/null
@@ -1,122 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { AbstractLayer } from '../layer';
-import { VectorLayer } from '../vector_layer/vector_layer';
-import { HeatmapStyle } from '../../styles/heatmap/heatmap_style';
-import { EMPTY_FEATURE_COLLECTION, LAYER_TYPE } from '../../../../common/constants';
-
-const SCALED_PROPERTY_NAME = '__kbn_heatmap_weight__'; //unique name to store scaled value for weighting
-
-export class HeatmapLayer extends VectorLayer {
- static type = LAYER_TYPE.HEATMAP;
-
- static createDescriptor(options) {
- const heatmapLayerDescriptor = super.createDescriptor(options);
- heatmapLayerDescriptor.type = HeatmapLayer.type;
- heatmapLayerDescriptor.style = HeatmapStyle.createDescriptor();
- return heatmapLayerDescriptor;
- }
-
- constructor({ layerDescriptor, source }) {
- super({ layerDescriptor, source });
- if (!layerDescriptor.style) {
- const defaultStyle = HeatmapStyle.createDescriptor();
- this._style = new HeatmapStyle(defaultStyle);
- } else {
- this._style = new HeatmapStyle(layerDescriptor.style);
- }
- }
-
- getStyleForEditing() {
- return this._style;
- }
-
- getStyle() {
- return this._style;
- }
-
- getCurrentStyle() {
- return this._style;
- }
-
- _getPropKeyOfSelectedMetric() {
- const metricfields = this.getSource().getMetricFields();
- return metricfields[0].getName();
- }
-
- _getHeatmapLayerId() {
- return this.makeMbLayerId('heatmap');
- }
-
- getMbLayerIds() {
- return [this._getHeatmapLayerId()];
- }
-
- ownsMbLayerId(mbLayerId) {
- return this._getHeatmapLayerId() === mbLayerId;
- }
-
- syncLayerWithMB(mbMap) {
- super._syncSourceBindingWithMb(mbMap);
-
- const heatmapLayerId = this._getHeatmapLayerId();
- if (!mbMap.getLayer(heatmapLayerId)) {
- mbMap.addLayer({
- id: heatmapLayerId,
- type: 'heatmap',
- source: this.getId(),
- paint: {},
- });
- }
-
- const mbSourceAfter = mbMap.getSource(this.getId());
- const sourceDataRequest = this.getSourceDataRequest();
- const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null;
- if (!featureCollection) {
- mbSourceAfter.setData(EMPTY_FEATURE_COLLECTION);
- return;
- }
-
- const propertyKey = this._getPropKeyOfSelectedMetric();
- const dataBoundToMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId());
- if (featureCollection !== dataBoundToMap) {
- let max = 1; //max will be at least one, since counts or sums will be at least one.
- for (let i = 0; i < featureCollection.features.length; i++) {
- max = Math.max(featureCollection.features[i].properties[propertyKey], max);
- }
- for (let i = 0; i < featureCollection.features.length; i++) {
- featureCollection.features[i].properties[SCALED_PROPERTY_NAME] =
- featureCollection.features[i].properties[propertyKey] / max;
- }
- mbSourceAfter.setData(featureCollection);
- }
-
- this.syncVisibilityWithMb(mbMap, heatmapLayerId);
- this.getCurrentStyle().setMBPaintProperties({
- mbMap,
- layerId: heatmapLayerId,
- propertyName: SCALED_PROPERTY_NAME,
- resolution: this.getSource().getGridResolution(),
- });
- mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha());
- mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom());
- }
-
- getLayerTypeIconName() {
- return 'heatmap';
- }
-
- async hasLegendDetails() {
- return true;
- }
-
- renderLegendDetails() {
- const metricFields = this.getSource().getMetricFields();
- return this.getCurrentStyle().renderLegendDetails(metricFields[0]);
- }
-}
diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts
new file mode 100644
index 0000000000000..8eebd7c57afd7
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts
@@ -0,0 +1,182 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Map as MbMap, GeoJSONSource as MbGeoJSONSource } from 'mapbox-gl';
+import { FeatureCollection } from 'geojson';
+import { AbstractLayer } from '../layer';
+import { HeatmapStyle } from '../../styles/heatmap/heatmap_style';
+import { EMPTY_FEATURE_COLLECTION, LAYER_TYPE } from '../../../../common/constants';
+import { HeatmapLayerDescriptor, MapQuery } from '../../../../common/descriptor_types';
+import { ESGeoGridSource } from '../../sources/es_geo_grid_source';
+import { addGeoJsonMbSource, syncVectorSource } from '../vector_layer';
+import { DataRequestContext } from '../../../actions';
+import { DataRequestAbortError } from '../../util/data_request';
+
+const SCALED_PROPERTY_NAME = '__kbn_heatmap_weight__'; // unique name to store scaled value for weighting
+
+export class HeatmapLayer extends AbstractLayer {
+ static type = LAYER_TYPE.HEATMAP;
+
+ private readonly _style: HeatmapStyle;
+
+ static createDescriptor(options: Partial) {
+ const heatmapLayerDescriptor = super.createDescriptor(options);
+ heatmapLayerDescriptor.type = HeatmapLayer.type;
+ heatmapLayerDescriptor.style = HeatmapStyle.createDescriptor();
+ return heatmapLayerDescriptor;
+ }
+
+ constructor({
+ layerDescriptor,
+ source,
+ }: {
+ layerDescriptor: HeatmapLayerDescriptor;
+ source: ESGeoGridSource;
+ }) {
+ super({ layerDescriptor, source });
+ if (!layerDescriptor.style) {
+ const defaultStyle = HeatmapStyle.createDescriptor();
+ this._style = new HeatmapStyle(defaultStyle);
+ } else {
+ this._style = new HeatmapStyle(layerDescriptor.style);
+ }
+ }
+
+ getSource(): ESGeoGridSource {
+ return super.getSource() as ESGeoGridSource;
+ }
+
+ getStyleForEditing() {
+ return this._style;
+ }
+
+ getStyle() {
+ return this._style;
+ }
+
+ getCurrentStyle() {
+ return this._style;
+ }
+
+ _getPropKeyOfSelectedMetric() {
+ const metricfields = this.getSource().getMetricFields();
+ return metricfields[0].getName();
+ }
+
+ _getHeatmapLayerId() {
+ return this.makeMbLayerId('heatmap');
+ }
+
+ getMbLayerIds() {
+ return [this._getHeatmapLayerId()];
+ }
+
+ ownsMbLayerId(mbLayerId: string) {
+ return this._getHeatmapLayerId() === mbLayerId;
+ }
+
+ ownsMbSourceId(mbSourceId: string) {
+ return this.getId() === mbSourceId;
+ }
+
+ async syncData(syncContext: DataRequestContext) {
+ if (this.isLoadingBounds()) {
+ return;
+ }
+
+ const sourceQuery = this.getQuery() as MapQuery;
+ try {
+ await syncVectorSource({
+ layerId: this.getId(),
+ layerName: await this.getDisplayName(this.getSource()),
+ prevDataRequest: this.getSourceDataRequest(),
+ requestMeta: {
+ ...syncContext.dataFilters,
+ fieldNames: this.getSource().getFieldNames(),
+ geogridPrecision: this.getSource().getGeoGridPrecision(syncContext.dataFilters.zoom),
+ sourceQuery: sourceQuery ? sourceQuery : undefined,
+ applyGlobalQuery: this.getSource().getApplyGlobalQuery(),
+ applyGlobalTime: this.getSource().getApplyGlobalTime(),
+ sourceMeta: this.getSource().getSyncMeta(),
+ },
+ syncContext,
+ source: this.getSource(),
+ });
+ } catch (error) {
+ if (!(error instanceof DataRequestAbortError)) {
+ throw error;
+ }
+ }
+ }
+
+ syncLayerWithMB(mbMap: MbMap) {
+ addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap);
+
+ const heatmapLayerId = this._getHeatmapLayerId();
+ if (!mbMap.getLayer(heatmapLayerId)) {
+ mbMap.addLayer({
+ id: heatmapLayerId,
+ type: 'heatmap',
+ source: this.getId(),
+ paint: {},
+ });
+ }
+
+ const mbGeoJSONSource = mbMap.getSource(this.getId()) as MbGeoJSONSource;
+ const sourceDataRequest = this.getSourceDataRequest();
+ const featureCollection = sourceDataRequest
+ ? (sourceDataRequest.getData() as FeatureCollection)
+ : null;
+ if (!featureCollection) {
+ mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION);
+ return;
+ }
+
+ const propertyKey = this._getPropKeyOfSelectedMetric();
+ const dataBoundToMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId());
+ if (featureCollection !== dataBoundToMap) {
+ let max = 1; // max will be at least one, since counts or sums will be at least one.
+ for (let i = 0; i < featureCollection.features.length; i++) {
+ max = Math.max(featureCollection.features[i].properties?.[propertyKey], max);
+ }
+ for (let i = 0; i < featureCollection.features.length; i++) {
+ if (featureCollection.features[i].properties) {
+ featureCollection.features[i].properties![SCALED_PROPERTY_NAME] =
+ featureCollection.features[i].properties![propertyKey] / max;
+ }
+ }
+ mbGeoJSONSource.setData(featureCollection);
+ }
+
+ this.syncVisibilityWithMb(mbMap, heatmapLayerId);
+ this.getCurrentStyle().setMBPaintProperties({
+ mbMap,
+ layerId: heatmapLayerId,
+ propertyName: SCALED_PROPERTY_NAME,
+ resolution: this.getSource().getGridResolution(),
+ });
+ mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha());
+ mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom());
+ }
+
+ getLayerTypeIconName() {
+ return 'heatmap';
+ }
+
+ async getFields() {
+ return this.getSource().getFields();
+ }
+
+ async hasLegendDetails() {
+ return true;
+ }
+
+ renderLegendDetails() {
+ const metricFields = this.getSource().getMetricFields();
+ return this.getCurrentStyle().renderLegendDetails(metricFields[0]);
+ }
+}
diff --git a/x-pack/plugins/maps_file_upload/server/models/import_data/index.js b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/index.ts
similarity index 83%
rename from x-pack/plugins/maps_file_upload/server/models/import_data/index.js
rename to x-pack/plugins/maps/public/classes/layers/heatmap_layer/index.ts
index c1ba4b84975e5..ba15d97a39219 100644
--- a/x-pack/plugins/maps_file_upload/server/models/import_data/index.js
+++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { importDataProvider } from './import_data';
+export { HeatmapLayer } from './heatmap_layer';
diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx
index aedf7af08b2c8..89c6d70a217c9 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx
@@ -21,6 +21,7 @@ import {
MAX_ZOOM,
MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER,
MIN_ZOOM,
+ SOURCE_BOUNDS_DATA_REQUEST_ID,
SOURCE_DATA_REQUEST_ID,
SOURCE_TYPES,
STYLE_TYPE,
@@ -66,6 +67,7 @@ export interface ILayer {
getImmutableSourceProperties(): Promise;
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null;
isLayerLoading(): boolean;
+ isLoadingBounds(): boolean;
isFilteredByGlobalTime(): Promise;
hasErrors(): boolean;
getErrors(): string;
@@ -401,6 +403,11 @@ export class AbstractLayer implements ILayer {
return this._dataRequests.some((dataRequest) => dataRequest.isLoading());
}
+ isLoadingBounds() {
+ const boundsDataRequest = this.getDataRequest(SOURCE_BOUNDS_DATA_REQUEST_ID);
+ return !!boundsDataRequest && boundsDataRequest.isLoading();
+ }
+
hasErrors(): boolean {
return _.get(this._descriptor, '__isInErrorState', false);
}
diff --git a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts
index a32ae15405fac..bed7599f89073 100644
--- a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts
+++ b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts
@@ -9,7 +9,6 @@ import { registerLayerWizard } from './layer_wizard_registry';
import { uploadLayerWizardConfig } from './file_upload_wizard';
// @ts-ignore
import { esDocumentsLayerWizardConfig } from '../sources/es_search_source';
-// @ts-ignore
import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from '../sources/es_geo_grid_source';
import { geoLineLayerWizardConfig } from '../sources/es_geo_line_source';
// @ts-ignore
diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts
index c312ddec42572..b9cfb0067abd2 100644
--- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts
+++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts
@@ -176,7 +176,6 @@ describe('createLayerDescriptor', () => {
__dataRequests: [],
alpha: 0.75,
id: '12345',
- joins: [],
label: '[Performance] Duration',
maxZoom: 24,
minZoom: 0,
diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts
index fd9147d62cc26..03870e7668189 100644
--- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts
+++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts
@@ -31,11 +31,9 @@ import { OBSERVABILITY_METRIC_TYPE } from './metric_select';
import { DISPLAY } from './display_select';
import { VectorStyle } from '../../../styles/vector/vector_style';
import { EMSFileSource } from '../../../sources/ems_file_source';
-// @ts-ignore
import { ESGeoGridSource } from '../../../sources/es_geo_grid_source';
-import { VectorLayer } from '../../vector_layer/vector_layer';
-// @ts-ignore
-import { HeatmapLayer } from '../../heatmap_layer/heatmap_layer';
+import { VectorLayer } from '../../vector_layer';
+import { HeatmapLayer } from '../../heatmap_layer';
import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style_defaults';
// redefining APM constant to avoid making maps app depend on APM plugin
diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts
index 74a66276459c7..b2283196a41dd 100644
--- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts
+++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts
@@ -22,7 +22,7 @@ import {
SYMBOLIZE_AS_TYPES,
VECTOR_STYLES,
} from '../../../../../common/constants';
-import { VectorLayer } from '../../vector_layer/vector_layer';
+import { VectorLayer } from '../../vector_layer';
import { VectorStyle } from '../../../styles/vector/vector_style';
// @ts-ignore
import { ESSearchSource } from '../../../sources/es_search_source';
diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
index d98396b960cbd..477b17ae03d7b 100644
--- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
@@ -15,7 +15,7 @@ import { EuiIcon } from '@elastic/eui';
import { Feature } from 'geojson';
import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style';
import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants';
-import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer';
+import { VectorLayer, VectorLayerArguments } from '../vector_layer';
import { ITiledSingleLayerVectorSource } from '../../sources/vector_source';
import { DataRequestContext } from '../../../actions';
import {
diff --git a/x-pack/plugins/maps/public/classes/util/assign_feature_ids.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts
similarity index 97%
rename from x-pack/plugins/maps/public/classes/util/assign_feature_ids.test.ts
rename to x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts
index 4fe3804968b81..137d443b39b91 100644
--- a/x-pack/plugins/maps/public/classes/util/assign_feature_ids.test.ts
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts
@@ -6,7 +6,7 @@
*/
import { assignFeatureIds } from './assign_feature_ids';
-import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants';
+import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
import { FeatureCollection, Feature, Point } from 'geojson';
const featureId = 'myFeature1';
diff --git a/x-pack/plugins/maps/public/classes/util/assign_feature_ids.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts
similarity index 96%
rename from x-pack/plugins/maps/public/classes/util/assign_feature_ids.ts
rename to x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts
index f6b7851159586..c40c8299ad04c 100644
--- a/x-pack/plugins/maps/public/classes/util/assign_feature_ids.ts
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts
@@ -7,7 +7,7 @@
import _ from 'lodash';
import { FeatureCollection, Feature } from 'geojson';
-import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants';
+import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
let idCounter = 0;
diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts
similarity index 63%
rename from x-pack/plugins/maps_file_upload/server/telemetry/index.ts
rename to x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts
index 83cd64c3f0e6f..4b509ba5dff00 100644
--- a/x-pack/plugins/maps_file_upload/server/telemetry/index.ts
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts
@@ -5,5 +5,5 @@
* 2.0.
*/
-export { registerFileUploadUsageCollector } from './file_upload_usage_collector';
-export { fileUploadTelemetryMappingsType } from './mappings';
+export { addGeoJsonMbSource, syncVectorSource } from './utils';
+export { IVectorLayer, VectorLayer, VectorLayerArguments } from './vector_layer';
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx
new file mode 100644
index 0000000000000..a3754b20de818
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx
@@ -0,0 +1,114 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FeatureCollection } from 'geojson';
+import { Map as MbMap } from 'mapbox-gl';
+import {
+ EMPTY_FEATURE_COLLECTION,
+ SOURCE_DATA_REQUEST_ID,
+ VECTOR_SHAPE_TYPE,
+} from '../../../../common/constants';
+import { VectorSourceRequestMeta } from '../../../../common/descriptor_types';
+import { DataRequestContext } from '../../../actions';
+import { IVectorSource } from '../../sources/vector_source';
+import { DataRequestAbortError } from '../../util/data_request';
+import { DataRequest } from '../../util/data_request';
+import { getCentroidFeatures } from '../../../../common/get_centroid_features';
+import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
+import { assignFeatureIds } from './assign_feature_ids';
+
+export function addGeoJsonMbSource(mbSourceId: string, mbLayerIds: string[], mbMap: MbMap) {
+ const mbSource = mbMap.getSource(mbSourceId);
+ if (!mbSource) {
+ mbMap.addSource(mbSourceId, {
+ type: 'geojson',
+ data: EMPTY_FEATURE_COLLECTION,
+ });
+ } else if (mbSource.type !== 'geojson') {
+ // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer.
+ mbLayerIds.forEach((mbLayerId) => {
+ if (mbMap.getLayer(mbLayerId)) {
+ mbMap.removeLayer(mbLayerId);
+ }
+ });
+
+ mbMap.removeSource(mbSourceId);
+ mbMap.addSource(mbSourceId, {
+ type: 'geojson',
+ data: EMPTY_FEATURE_COLLECTION,
+ });
+ }
+}
+
+export async function syncVectorSource({
+ layerId,
+ layerName,
+ prevDataRequest,
+ requestMeta,
+ syncContext,
+ source,
+}: {
+ layerId: string;
+ layerName: string;
+ prevDataRequest: DataRequest | undefined;
+ requestMeta: VectorSourceRequestMeta;
+ syncContext: DataRequestContext;
+ source: IVectorSource;
+}): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> {
+ const {
+ startLoading,
+ stopLoading,
+ onLoadError,
+ registerCancelCallback,
+ isRequestStillActive,
+ } = syncContext;
+ const dataRequestId = SOURCE_DATA_REQUEST_ID;
+ const requestToken = Symbol(`${layerId}-${dataRequestId}`);
+ const canSkipFetch = await canSkipSourceUpdate({
+ source,
+ prevDataRequest,
+ nextMeta: requestMeta,
+ });
+ if (canSkipFetch) {
+ return {
+ refreshed: false,
+ featureCollection: prevDataRequest
+ ? (prevDataRequest.getData() as FeatureCollection)
+ : EMPTY_FEATURE_COLLECTION,
+ };
+ }
+
+ try {
+ startLoading(dataRequestId, requestToken, requestMeta);
+ const { data: sourceFeatureCollection, meta } = await source.getGeoJsonWithMeta(
+ layerName,
+ requestMeta,
+ registerCancelCallback.bind(null, requestToken),
+ () => {
+ return isRequestStillActive(dataRequestId, requestToken);
+ }
+ );
+ const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection);
+ const supportedShapes = await source.getSupportedShapeTypes();
+ if (
+ supportedShapes.includes(VECTOR_SHAPE_TYPE.LINE) ||
+ supportedShapes.includes(VECTOR_SHAPE_TYPE.POLYGON)
+ ) {
+ layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection));
+ }
+ stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta);
+ return {
+ refreshed: true,
+ featureCollection: layerFeatureCollection,
+ };
+ } catch (error) {
+ if (!(error instanceof DataRequestAbortError)) {
+ onLoadError(dataRequestId, requestToken, error.message);
+ }
+ throw error;
+ }
+}
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
index ee1cda6eaee43..7e87d99fd4f93 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx
@@ -13,10 +13,8 @@ import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AbstractLayer } from '../layer';
import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style';
-import { getCentroidFeatures } from '../../../../common/get_centroid_features';
import {
FEATURE_ID_PROPERTY_NAME,
- SOURCE_DATA_REQUEST_ID,
SOURCE_META_DATA_REQUEST_ID,
SOURCE_FORMATTERS_DATA_REQUEST_ID,
SOURCE_BOUNDS_DATA_REQUEST_ID,
@@ -25,10 +23,8 @@ import {
KBN_TOO_MANY_FEATURES_PROPERTY,
LAYER_TYPE,
FIELD_ORIGIN,
- LAYER_STYLE_TYPE,
KBN_TOO_MANY_FEATURES_IMAGE_ID,
FieldFormatter,
- VECTOR_SHAPE_TYPE,
} from '../../../../common/constants';
import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property';
import { DataRequestAbortError } from '../../util/data_request';
@@ -37,7 +33,6 @@ import {
canSkipStyleMetaUpdate,
canSkipFormattersUpdate,
} from '../../util/can_skip_fetch';
-import { assignFeatureIds } from '../../util/assign_feature_ids';
import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds';
import {
getCentroidFilterExpression,
@@ -65,6 +60,7 @@ import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_st
import { IESSource } from '../../sources/es_source';
import { PropertiesMap } from '../../../../common/elasticsearch_util';
import { ITermJoinSource } from '../../sources/term_join_source';
+import { addGeoJsonMbSource, syncVectorSource } from './utils';
interface SourceResult {
refreshed: boolean;
@@ -81,6 +77,7 @@ export interface VectorLayerArguments {
source: IVectorSource;
joins?: InnerJoin[];
layerDescriptor: VectorLayerDescriptor;
+ chartsPaletteServiceGetColor?: (value: string) => string | null;
}
export interface IVectorLayer extends ILayer {
@@ -94,7 +91,7 @@ export interface IVectorLayer extends ILayer {
hasJoins(): boolean;
}
-export class VectorLayer extends AbstractLayer {
+export class VectorLayer extends AbstractLayer implements IVectorLayer {
static type = LAYER_TYPE.VECTOR;
protected readonly _style: IVectorStyle;
@@ -119,13 +116,23 @@ export class VectorLayer extends AbstractLayer {
return layerDescriptor as VectorLayerDescriptor;
}
- constructor({ layerDescriptor, source, joins = [] }: VectorLayerArguments) {
+ constructor({
+ layerDescriptor,
+ source,
+ joins = [],
+ chartsPaletteServiceGetColor,
+ }: VectorLayerArguments) {
super({
layerDescriptor,
source,
});
this._joins = joins;
- this._style = new VectorStyle(layerDescriptor.style, source, this);
+ this._style = new VectorStyle(
+ layerDescriptor.style,
+ source,
+ this,
+ chartsPaletteServiceGetColor
+ );
}
getSource(): IVectorSource {
@@ -277,11 +284,6 @@ export class VectorLayer extends AbstractLayer {
return bounds;
}
- isLoadingBounds() {
- const boundsDataRequest = this.getDataRequest(SOURCE_BOUNDS_DATA_REQUEST_ID);
- return !!boundsDataRequest && boundsDataRequest.isLoading();
- }
-
async getLeftJoinFields() {
return await this.getSource().getLeftJoinFields();
}
@@ -409,11 +411,9 @@ export class VectorLayer extends AbstractLayer {
source: IVectorSource,
style: IVectorStyle
): VectorSourceRequestMeta {
- const styleFieldNames =
- style.getType() === LAYER_STYLE_TYPE.VECTOR ? style.getSourceFieldNames() : [];
const fieldNames = [
...source.getFieldNames(),
- ...styleFieldNames,
+ ...style.getSourceFieldNames(),
...this.getValidJoins().map((join) => join.getLeftField().getName()),
];
@@ -474,82 +474,11 @@ export class VectorLayer extends AbstractLayer {
}
}
- async _syncSource(
- syncContext: DataRequestContext,
- source: IVectorSource,
- style: IVectorStyle
- ): Promise {
- const {
- startLoading,
- stopLoading,
- onLoadError,
- registerCancelCallback,
- dataFilters,
- isRequestStillActive,
- } = syncContext;
- const dataRequestId = SOURCE_DATA_REQUEST_ID;
- const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`);
- const searchFilters: VectorSourceRequestMeta = this._getSearchFilters(
- dataFilters,
- source,
- style
- );
- const prevDataRequest = this.getSourceDataRequest();
- const canSkipFetch = await canSkipSourceUpdate({
- source,
- prevDataRequest,
- nextMeta: searchFilters,
- });
- if (canSkipFetch) {
- return {
- refreshed: false,
- featureCollection: prevDataRequest
- ? (prevDataRequest.getData() as FeatureCollection)
- : EMPTY_FEATURE_COLLECTION,
- };
- }
-
- try {
- startLoading(dataRequestId, requestToken, searchFilters);
- const layerName = await this.getDisplayName(source);
- const { data: sourceFeatureCollection, meta } = await source.getGeoJsonWithMeta(
- layerName,
- searchFilters,
- registerCancelCallback.bind(null, requestToken),
- () => {
- return isRequestStillActive(dataRequestId, requestToken);
- }
- );
- const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection);
- const supportedShapes = await source.getSupportedShapeTypes();
- if (
- supportedShapes.includes(VECTOR_SHAPE_TYPE.LINE) ||
- supportedShapes.includes(VECTOR_SHAPE_TYPE.POLYGON)
- ) {
- layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection));
- }
- stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta);
- return {
- refreshed: true,
- featureCollection: layerFeatureCollection,
- };
- } catch (error) {
- if (!(error instanceof DataRequestAbortError)) {
- onLoadError(dataRequestId, requestToken, error.message);
- }
- throw error;
- }
- }
-
async _syncSourceStyleMeta(
syncContext: DataRequestContext,
source: IVectorSource,
style: IVectorStyle
) {
- if (this.getCurrentStyle().getType() !== LAYER_STYLE_TYPE.VECTOR) {
- return;
- }
-
const sourceQuery = this.getQuery() as MapQuery;
return this._syncStyleMeta({
source,
@@ -654,10 +583,6 @@ export class VectorLayer extends AbstractLayer {
source: IVectorSource,
style: IVectorStyle
) {
- if (style.getType() !== LAYER_STYLE_TYPE.VECTOR) {
- return;
- }
-
return this._syncFormatters({
source,
dataRequestId: SOURCE_FORMATTERS_DATA_REQUEST_ID,
@@ -762,7 +687,14 @@ export class VectorLayer extends AbstractLayer {
try {
await this._syncSourceStyleMeta(syncContext, source, style);
await this._syncSourceFormatters(syncContext, source, style);
- const sourceResult = await this._syncSource(syncContext, source, style);
+ const sourceResult = await syncVectorSource({
+ layerId: this.getId(),
+ layerName: await this.getDisplayName(source),
+ prevDataRequest: this.getSourceDataRequest(),
+ requestMeta: this._getSearchFilters(syncContext.dataFilters, source, style),
+ syncContext,
+ source,
+ });
if (
!sourceResult.featureCollection ||
!sourceResult.featureCollection.features.length ||
@@ -1050,31 +982,8 @@ export class VectorLayer extends AbstractLayer {
this._setMbCentroidProperties(mbMap);
}
- _syncSourceBindingWithMb(mbMap: MbMap) {
- const mbSource = mbMap.getSource(this._getMbSourceId());
- if (!mbSource) {
- mbMap.addSource(this._getMbSourceId(), {
- type: 'geojson',
- data: EMPTY_FEATURE_COLLECTION,
- });
- } else if (mbSource.type !== 'geojson') {
- // Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer.
- this.getMbLayerIds().forEach((mbLayerId) => {
- if (mbMap.getLayer(mbLayerId)) {
- mbMap.removeLayer(mbLayerId);
- }
- });
-
- mbMap.removeSource(this._getMbSourceId());
- mbMap.addSource(this._getMbSourceId(), {
- type: 'geojson',
- data: EMPTY_FEATURE_COLLECTION,
- });
- }
- }
-
syncLayerWithMB(mbMap: MbMap) {
- this._syncSourceBindingWithMb(mbMap);
+ addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap);
this._syncFeatureCollectionWithMb(mbMap);
this._syncStylePropertiesWithMb(mbMap);
}
diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
index 3acc3c59e5930..d4cf4dbee7943 100644
--- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import { VectorLayer } from '../../layers/vector_layer';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
import { EMSFileCreateSourceEditor } from './create_source_editor';
import { EMSFileSource, getSourceTitle } from './ems_file_source';
diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx
index 5a0a3ed8df596..e711fb900e39a 100644
--- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx
@@ -7,7 +7,7 @@
import { EMSFileSource } from './ems_file_source';
-jest.mock('../../layers/vector_layer/vector_layer', () => {});
+jest.mock('../../layers/vector_layer', () => {});
function makeEMSFileSource(tooltipProperties: string[]) {
const emsFileSource = new EMSFileSource({ tooltipProperties });
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx
index 8951b7b278459..36dd28cb5bbf1 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx
@@ -9,10 +9,9 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
// @ts-ignore
import { CreateSourceEditor } from './create_source_editor';
-// @ts-ignore
import { ESGeoGridSource, clustersTitle } from './es_geo_grid_source';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import { VectorLayer } from '../../layers/vector_layer';
import {
ESGeoGridSourceDescriptor,
ColorDynamicOptions,
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx
index 83a7e02383f77..8fc26f3593750 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx
@@ -9,11 +9,9 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
// @ts-ignore
import { CreateSourceEditor } from './create_source_editor';
-// @ts-ignore
import { ESGeoGridSource, heatmapTitle } from './es_geo_grid_source';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
-// @ts-ignore
-import { HeatmapLayer } from '../../layers/heatmap_layer/heatmap_layer';
+import { HeatmapLayer } from '../../layers/heatmap_layer';
import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types';
import { LAYER_WIZARD_CATEGORY, RENDER_AS } from '../../../../common/constants';
import { HeatmapLayerIcon } from '../../layers/icons/heatmap_layer_icon';
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.ts
similarity index 100%
rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.js
rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.ts
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx
index 6a1dfc74271d8..8da7037a5a34c 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx
@@ -12,7 +12,7 @@ import { ESGeoLineSource, geoLineTitle, REQUIRES_GOLD_LICENSE_MSG } from './es_g
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
import { LAYER_WIZARD_CATEGORY, STYLE_TYPE, VECTOR_STYLES } from '../../../../common/constants';
import { VectorStyle } from '../../styles/vector/vector_style';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import { VectorLayer } from '../../layers/vector_layer';
import { getIsGoldPlus } from '../../../licensed_features';
import { TracksLayerIcon } from '../../layers/icons/tracks_layer_icon';
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
index 49b161711481c..c94c7859a85e7 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import { VectorLayer } from '../../layers/vector_layer';
// @ts-ignore
import { ESPewPewSource, sourceTitle } from './es_pew_pew_source';
import { VectorStyle } from '../../styles/vector/vector_style';
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts
index 2734af5742dbb..41b4e8d7a318a 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts
@@ -9,7 +9,7 @@ import { Query } from 'src/plugins/data/public';
import { LayerDescriptor } from '../../../../common/descriptor_types';
import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants';
import { ESSearchSource } from './es_search_source';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import { VectorLayer } from '../../layers/vector_layer';
import { getIsGoldPlus } from '../../../licensed_features';
export interface CreateLayerDescriptorParams {
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx
index d01ed459e3171..c0606b5f4aec6 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx
@@ -13,7 +13,7 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re
// @ts-ignore
import { ESSearchSource, sourceTitle } from './es_search_source';
import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import { VectorLayer } from '../../layers/vector_layer';
import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants';
import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer';
import { DocumentsLayerIcon } from '../../layers/icons/documents_layer_icon';
diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js
index a7994db286112..1f4a1ab7c9afa 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js
@@ -7,7 +7,7 @@
import { ESTermSource, extractPropertiesMap } from './es_term_source';
-jest.mock('../../layers/vector_layer/vector_layer', () => {});
+jest.mock('../../layers/vector_layer', () => {});
const indexPatternTitle = 'myIndex';
const termFieldName = 'myTermField';
diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
index b41f599ac3d75..907b80e6405a6 100644
--- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
@@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
// @ts-ignore
import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import { VectorLayer } from '../../layers/vector_layer';
// @ts-ignore
import { CreateSourceEditor } from './create_source_editor';
import { getKibanaRegionList } from '../../../meta';
diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx
index f30040cf93b57..fe581a1807b28 100644
--- a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx
@@ -31,7 +31,7 @@ export class HeatmapStyle implements IStyle {
this._descriptor = HeatmapStyle.createDescriptor(descriptor.colorRampName);
}
- static createDescriptor(colorRampName: string) {
+ static createDescriptor(colorRampName?: string) {
return {
type: LAYER_STYLE_TYPE.HEATMAP,
colorRampName: colorRampName ? colorRampName : DEFAULT_HEATMAP_COLOR_RAMP_NAME,
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx
index 3cfae4a836042..d0d3a7c2abe06 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { StyleProperties, VectorStyleEditor } from './vector_style_editor';
import { getDefaultStaticProperties } from '../vector_style_defaults';
-import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../../../layers/vector_layer';
import { IVectorSource } from '../../../sources/vector_source';
import {
FIELD_ORIGIN,
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx
index b36f3a38e2783..91bcc2dc06859 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx
@@ -49,7 +49,7 @@ import { SymbolizeAsProperty } from '../properties/symbolize_as_property';
import { LabelBorderSizeProperty } from '../properties/label_border_size_property';
import { StaticTextProperty } from '../properties/static_text_property';
import { StaticSizeProperty } from '../properties/static_size_property';
-import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../../../layers/vector_layer';
export interface StyleProperties {
[key: string]: IStyleProperty;
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx
index 03b7ce17063c3..b7e0133881ee1 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx
@@ -24,7 +24,7 @@ import {
} from '../../../../../common/constants';
import { mockField, MockLayer, MockStyle } from './test_helpers/test_util';
import { ColorDynamicOptions } from '../../../../../common/descriptor_types';
-import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../../../layers/vector_layer';
import { IField } from '../../../fields/field';
const makeProperty = (options: ColorDynamicOptions, style?: MockStyle, field?: IField) => {
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx
index cac56ad1c8a57..d654cdc6bff51 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx
@@ -16,7 +16,12 @@ import {
getPercentilesMbColorRampStops,
getColorPalette,
} from '../../color_palettes';
-import { COLOR_MAP_TYPE, DATA_MAPPING_FUNCTION } from '../../../../../common/constants';
+import {
+ COLOR_MAP_TYPE,
+ DATA_MAPPING_FUNCTION,
+ FieldFormatter,
+ VECTOR_STYLES,
+} from '../../../../../common/constants';
import {
isCategoricalStopsInvalid,
getOtherCategoryLabel,
@@ -26,6 +31,8 @@ import { Break, BreakedLegend } from '../components/legend/breaked_legend';
import { ColorDynamicOptions, OrdinalColorStop } from '../../../../../common/descriptor_types';
import { LegendProps } from './style_property';
import { getOrdinalSuffix } from '../../../util/ordinal_suffix';
+import { IField } from '../../../fields/field';
+import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
const UP_TO = i18n.translate('xpack.maps.legend.upto', {
defaultMessage: 'up to',
@@ -34,6 +41,20 @@ const EMPTY_STOPS = { stops: [], defaultColor: null };
const RGBA_0000 = 'rgba(0,0,0,0)';
export class DynamicColorProperty extends DynamicStyleProperty {
+ private readonly _chartsPaletteServiceGetColor?: (value: string) => string | null;
+
+ constructor(
+ options: ColorDynamicOptions,
+ styleName: VECTOR_STYLES,
+ field: IField | null,
+ vectorLayer: IVectorLayer,
+ getFieldFormatter: (fieldName: string) => null | FieldFormatter,
+ chartsPaletteServiceGetColor?: (value: string) => string | null
+ ) {
+ super(options, styleName, field, vectorLayer, getFieldFormatter);
+ this._chartsPaletteServiceGetColor = chartsPaletteServiceGetColor;
+ }
+
syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
const color = this._getMbColor();
mbMap.setPaintProperty(mbLayerId, 'circle-color', color);
@@ -260,12 +281,16 @@ export class DynamicColorProperty extends DynamicStyleProperty {
- if (stop !== null) {
+ stops.forEach(({ stop, color }: { stop: string | number | null; color: string | null }) => {
+ if (stop !== null && color != null) {
breaks.push({
color,
symbolId,
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx
index fc4d495f1e40a..46339c5a4a20d 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx
@@ -20,7 +20,7 @@ import { DynamicIconProperty } from './dynamic_icon_property';
import { mockField, MockLayer } from './test_helpers/test_util';
import { IconDynamicOptions } from '../../../../../common/descriptor_types';
import { IField } from '../../../fields/field';
-import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../../../layers/vector_layer';
const makeProperty = (options: Partial, field: IField = mockField) => {
const defaultOptions: IconDynamicOptions = {
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx
index 40d72a357218f..64a3e0cf0e322 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx
@@ -21,7 +21,7 @@ import { IField } from '../../../fields/field';
import { Map as MbMap } from 'mapbox-gl';
import { SizeDynamicOptions } from '../../../../../common/descriptor_types';
import { mockField, MockLayer, MockStyle } from './test_helpers/test_util';
-import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../../../layers/vector_layer';
export class MockMbMap {
_paintPropertyCalls: unknown[];
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx
index 52b78b4211a2d..7076775dcce31 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx
@@ -20,7 +20,7 @@ import {
import { FieldFormatter, MB_LOOKUP_FUNCTION, VECTOR_STYLES } from '../../../../../common/constants';
import { SizeDynamicOptions } from '../../../../../common/descriptor_types';
import { IField } from '../../../fields/field';
-import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../../../layers/vector_layer';
export class DynamicSizeProperty extends DynamicStyleProperty {
private readonly _isSymbolizedAsIcon: boolean;
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx
index f62b17ee05ad6..9ffd9a0f1b345 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx
@@ -34,7 +34,7 @@ import {
StyleMetaData,
} from '../../../../../common/descriptor_types';
import { IField } from '../../../fields/field';
-import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../../../layers/vector_layer';
import { InnerJoin } from '../../../joins/inner_join';
import { IVectorStyle } from '../vector_style';
import { getComputedFieldName } from '../style_util';
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx
index cef5f5048e9af..692be08d07bc6 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx
+++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx
@@ -70,7 +70,7 @@ import { DataRequest } from '../../util/data_request';
import { IStyle } from '../style';
import { IStyleProperty } from './properties/style_property';
import { IField } from '../../fields/field';
-import { IVectorLayer } from '../../layers/vector_layer/vector_layer';
+import { IVectorLayer } from '../../layers/vector_layer';
import { IVectorSource } from '../../sources/vector_source';
import { createStyleFieldsHelper, StyleFieldsHelper } from './style_fields_helper';
import { IESAggField } from '../../fields/agg';
@@ -178,7 +178,8 @@ export class VectorStyle implements IVectorStyle {
constructor(
descriptor: VectorStyleDescriptor | null,
source: IVectorSource,
- layer: IVectorLayer
+ layer: IVectorLayer,
+ chartsPaletteServiceGetColor?: (value: string) => string | null
) {
this._source = source;
this._layer = layer;
@@ -197,11 +198,13 @@ export class VectorStyle implements IVectorStyle {
);
this._lineColorStyleProperty = this._makeColorProperty(
this._descriptor.properties[VECTOR_STYLES.LINE_COLOR],
- VECTOR_STYLES.LINE_COLOR
+ VECTOR_STYLES.LINE_COLOR,
+ chartsPaletteServiceGetColor
);
this._fillColorStyleProperty = this._makeColorProperty(
this._descriptor.properties[VECTOR_STYLES.FILL_COLOR],
- VECTOR_STYLES.FILL_COLOR
+ VECTOR_STYLES.FILL_COLOR,
+ chartsPaletteServiceGetColor
);
this._lineWidthStyleProperty = this._makeSizeProperty(
this._descriptor.properties[VECTOR_STYLES.LINE_WIDTH],
@@ -230,11 +233,13 @@ export class VectorStyle implements IVectorStyle {
);
this._labelColorStyleProperty = this._makeColorProperty(
this._descriptor.properties[VECTOR_STYLES.LABEL_COLOR],
- VECTOR_STYLES.LABEL_COLOR
+ VECTOR_STYLES.LABEL_COLOR,
+ chartsPaletteServiceGetColor
);
this._labelBorderColorStyleProperty = this._makeColorProperty(
this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR],
- VECTOR_STYLES.LABEL_BORDER_COLOR
+ VECTOR_STYLES.LABEL_BORDER_COLOR,
+ chartsPaletteServiceGetColor
);
this._labelBorderSizeStyleProperty = new LabelBorderSizeProperty(
this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_SIZE].options,
@@ -890,7 +895,8 @@ export class VectorStyle implements IVectorStyle {
_makeColorProperty(
descriptor: ColorStylePropertyDescriptor | undefined,
- styleName: VECTOR_STYLES
+ styleName: VECTOR_STYLES,
+ chartsPaletteServiceGetColor?: (value: string) => string | null
) {
if (!descriptor || !descriptor.options) {
return new StaticColorProperty({ color: '' }, styleName);
@@ -904,7 +910,8 @@ export class VectorStyle implements IVectorStyle {
styleName,
field,
this._layer,
- this._getFieldFormatter
+ this._getFieldFormatter,
+ chartsPaletteServiceGetColor
);
} else {
throw new Error(`${descriptor} not implemented`);
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
index a1d65bf08c458..b769ac489f565 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
@@ -37,6 +37,7 @@ import {
import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors';
import {
getInspectorAdapters,
+ setChartsPaletteServiceGetColor,
setEventHandlers,
EventHandlers,
} from '../reducers/non_serializable_instances';
@@ -54,7 +55,12 @@ import {
RawValue,
} from '../../common/constants';
import { RenderToolTipContent } from '../classes/tooltips/tooltip_property';
-import { getUiActions, getCoreI18n, getHttp } from '../kibana_services';
+import {
+ getUiActions,
+ getCoreI18n,
+ getHttp,
+ getChartsPaletteServiceGetColor,
+} from '../kibana_services';
import { LayerDescriptor } from '../../common/descriptor_types';
import { MapContainer } from '../connected_components/map_container';
import { SavedMap } from '../routes/map_page';
@@ -83,6 +89,7 @@ export class MapEmbeddable
private _prevQuery?: Query;
private _prevRefreshConfig?: RefreshInterval;
private _prevFilters?: Filter[];
+ private _prevSyncColors?: boolean;
private _prevSearchSessionId?: string;
private _domNode?: HTMLElement;
private _unsubscribeFromStore?: Unsubscribe;
@@ -126,6 +133,8 @@ export class MapEmbeddable
}
private _initializeStore() {
+ this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors);
+
const store = this._savedMap.getStore();
store.dispatch(setReadOnly(true));
store.dispatch(disableScrollZoom());
@@ -221,6 +230,10 @@ export class MapEmbeddable
if (this.input.refreshConfig && !_.isEqual(this.input.refreshConfig, this._prevRefreshConfig)) {
this._dispatchSetRefreshConfig(this.input.refreshConfig);
}
+
+ if (this.input.syncColors !== this._prevSyncColors) {
+ this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors);
+ }
}
_dispatchSetQuery({
@@ -261,6 +274,19 @@ export class MapEmbeddable
);
}
+ async _dispatchSetChartsPaletteServiceGetColor(syncColors?: boolean) {
+ this._prevSyncColors = syncColors;
+ const chartsPaletteServiceGetColor = syncColors
+ ? await getChartsPaletteServiceGetColor()
+ : null;
+ if (syncColors !== this._prevSyncColors) {
+ return;
+ }
+ this._savedMap
+ .getStore()
+ .dispatch(setChartsPaletteServiceGetColor(chartsPaletteServiceGetColor));
+ }
+
/**
*
* @param {HTMLElement} domNode
diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts
index 632a5f5382f73..1fbca669b0d8e 100644
--- a/x-pack/plugins/maps/public/kibana_services.ts
+++ b/x-pack/plugins/maps/public/kibana_services.ts
@@ -11,6 +11,7 @@ import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config';
import { MapsConfigType } from '../config';
import { MapsPluginStartDependencies } from './plugin';
import { EMSSettings } from '../common/ems_settings';
+import { PaletteRegistry } from '../../../../src/plugins/charts/public';
let kibanaVersion: string;
export const setKibanaVersion = (version: string) => (kibanaVersion = version);
@@ -26,7 +27,7 @@ export const getIndexPatternService = () => pluginsStart.data.indexPatterns;
export const getAutocompleteService = () => pluginsStart.data.autocomplete;
export const getInspector = () => pluginsStart.inspector;
export const getFileUploadComponent = async () => {
- return await pluginsStart.mapsFileUpload.getFileUploadComponent();
+ return await pluginsStart.fileUpload.getFileUploadComponent();
};
export const getUiSettings = () => coreStart.uiSettings;
export const getIsDarkMode = () => getUiSettings().get('theme:darkMode', false);
@@ -83,3 +84,22 @@ export const getShareService = () => pluginsStart.share;
export const getIsAllowByValueEmbeddables = () =>
pluginsStart.dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables;
+
+export async function getChartsPaletteServiceGetColor(): Promise<
+ ((value: string) => string) | null
+> {
+ const paletteRegistry: PaletteRegistry | null = pluginsStart.charts
+ ? await pluginsStart.charts.palettes.getPalettes()
+ : null;
+ if (!paletteRegistry) {
+ return null;
+ }
+
+ const paletteDefinition = paletteRegistry.get('default');
+ const chartConfiguration = { syncColors: true };
+ return (value: string) => {
+ const series = [{ name: value, rankAtDepth: 0, totalSeriesAtDepth: 1 }];
+ const color = paletteDefinition.getColor(series, chartConfiguration);
+ return color ? color : '#3d3d3d';
+ };
+}
diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts
index 8889d1d44f10f..12cff9edf55ff 100644
--- a/x-pack/plugins/maps/public/plugin.ts
+++ b/x-pack/plugins/maps/public/plugin.ts
@@ -54,7 +54,7 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public';
-import { StartContract as FileUploadStartContract } from '../../maps_file_upload/public';
+import { StartContract as FileUploadStartContract } from '../../file_upload/public';
import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public';
import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public';
import {
@@ -64,6 +64,7 @@ import {
} from './licensed_features';
import { EMSSettings } from '../common/ems_settings';
import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public';
+import { ChartsPluginStart } from '../../../../src/plugins/charts/public';
export interface MapsPluginSetupDependencies {
inspector: InspectorSetupContract;
@@ -76,9 +77,10 @@ export interface MapsPluginSetupDependencies {
}
export interface MapsPluginStartDependencies {
+ charts: ChartsPluginStart;
data: DataPublicPluginStart;
embeddable: EmbeddableStart;
- mapsFileUpload: FileUploadStartContract;
+ fileUpload: FileUploadStartContract;
inspector: InspectorStartContract;
licensing: LicensingPluginStart;
navigation: NavigationPublicPluginStart;
diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts b/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts
index 54a90946a5a89..9808a5e09b8ab 100644
--- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts
+++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts
@@ -15,6 +15,7 @@ export type NonSerializableState = {
inspectorAdapters: Adapters;
cancelRequestCallbacks: Map {}>; // key is request token, value is cancel callback
eventHandlers: Partial;
+ chartsPaletteServiceGetColor: (value: string) => string | null;
};
export interface ResultMeta {
@@ -58,6 +59,14 @@ export function getInspectorAdapters(state: MapStoreState): Adapters;
export function getEventHandlers(state: MapStoreState): Partial;
+export function getChartsPaletteServiceGetColor(
+ state: MapStoreState
+): (value: string) => string | null;
+
+export function setChartsPaletteServiceGetColor(
+ chartsPaletteServiceGetColor: ((value: string) => string) | null
+): AnyAction;
+
export function cancelRequest(requestToken?: symbol): AnyAction;
export function registerCancelCallback(requestToken: symbol, callback: () => void): AnyAction;
diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js
index 46846a8df3f23..4cc4e91a308a5 100644
--- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js
+++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js
@@ -12,6 +12,7 @@ import { getShowMapsInspectorAdapter } from '../kibana_services';
const REGISTER_CANCEL_CALLBACK = 'REGISTER_CANCEL_CALLBACK';
const UNREGISTER_CANCEL_CALLBACK = 'UNREGISTER_CANCEL_CALLBACK';
const SET_EVENT_HANDLERS = 'SET_EVENT_HANDLERS';
+const SET_CHARTS_PALETTE_SERVICE_GET_COLOR = 'SET_CHARTS_PALETTE_SERVICE_GET_COLOR';
function createInspectorAdapters() {
const inspectorAdapters = {
@@ -30,6 +31,7 @@ export function nonSerializableInstances(state, action = {}) {
inspectorAdapters: createInspectorAdapters(),
cancelRequestCallbacks: new Map(), // key is request token, value is cancel callback
eventHandlers: {},
+ chartsPaletteServiceGetColor: null,
};
}
@@ -50,6 +52,12 @@ export function nonSerializableInstances(state, action = {}) {
eventHandlers: action.eventHandlers,
};
}
+ case SET_CHARTS_PALETTE_SERVICE_GET_COLOR: {
+ return {
+ ...state,
+ chartsPaletteServiceGetColor: action.chartsPaletteServiceGetColor,
+ };
+ }
default:
return state;
}
@@ -68,6 +76,11 @@ export const getEventHandlers = ({ nonSerializableInstances }) => {
return nonSerializableInstances.eventHandlers;
};
+export function getChartsPaletteServiceGetColor({ nonSerializableInstances }) {
+ console.log('getChartsPaletteServiceGetColor', nonSerializableInstances);
+ return nonSerializableInstances.chartsPaletteServiceGetColor;
+}
+
// Actions
export const registerCancelCallback = (requestToken, callback) => {
return {
@@ -104,3 +117,10 @@ export const setEventHandlers = (eventHandlers = {}) => {
eventHandlers,
};
};
+
+export function setChartsPaletteServiceGetColor(chartsPaletteServiceGetColor) {
+ return {
+ type: SET_CHARTS_PALETTE_SERVICE_GET_COLOR,
+ chartsPaletteServiceGetColor,
+ };
+}
diff --git a/x-pack/plugins/maps/public/reducers/store.js b/x-pack/plugins/maps/public/reducers/store.js
index 3c9b5d1b98e29..4e355add59fee 100644
--- a/x-pack/plugins/maps/public/reducers/store.js
+++ b/x-pack/plugins/maps/public/reducers/store.js
@@ -15,6 +15,7 @@ import { MAP_DESTROYED } from '../actions';
export const DEFAULT_MAP_STORE_STATE = {
ui: { ...DEFAULT_MAP_UI_STATE },
map: { ...DEFAULT_MAP_STATE },
+ nonSerializableInstances: {},
};
export function createMapStore() {
diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts
index eb11ee61d9deb..c2f5fc02c5df2 100644
--- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts
+++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts
@@ -5,17 +5,12 @@
* 2.0.
*/
-jest.mock('../classes/layers/vector_layer/vector_layer', () => {});
+jest.mock('../classes/layers/vector_layer', () => {});
jest.mock('../classes/layers/tiled_vector_layer/tiled_vector_layer', () => {});
jest.mock('../classes/layers/blended_vector_layer/blended_vector_layer', () => {});
-jest.mock('../classes/layers/heatmap_layer/heatmap_layer', () => {});
+jest.mock('../classes/layers/heatmap_layer', () => {});
jest.mock('../classes/layers/vector_tile_layer/vector_tile_layer', () => {});
jest.mock('../classes/joins/inner_join', () => {});
-jest.mock('../reducers/non_serializable_instances', () => ({
- getInspectorAdapters: () => {
- return {};
- },
-}));
jest.mock('../kibana_services', () => ({
getTimeFilter: () => ({
getTime: () => {
diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts
index 34af789f6834f..f53f39ad2fc0c 100644
--- a/x-pack/plugins/maps/public/selectors/map_selectors.ts
+++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts
@@ -12,13 +12,15 @@ import { Adapters } from 'src/plugins/inspector/public';
import { TileLayer } from '../classes/layers/tile_layer/tile_layer';
// @ts-ignore
import { VectorTileLayer } from '../classes/layers/vector_tile_layer/vector_tile_layer';
-import { IVectorLayer, VectorLayer } from '../classes/layers/vector_layer/vector_layer';
+import { IVectorLayer, VectorLayer } from '../classes/layers/vector_layer';
import { VectorStyle } from '../classes/styles/vector/vector_style';
-// @ts-ignore
-import { HeatmapLayer } from '../classes/layers/heatmap_layer/heatmap_layer';
+import { HeatmapLayer } from '../classes/layers/heatmap_layer';
import { BlendedVectorLayer } from '../classes/layers/blended_vector_layer/blended_vector_layer';
import { getTimeFilter } from '../kibana_services';
-import { getInspectorAdapters } from '../reducers/non_serializable_instances';
+import {
+ getChartsPaletteServiceGetColor,
+ getInspectorAdapters,
+} from '../reducers/non_serializable_instances';
import { TiledVectorLayer } from '../classes/layers/tiled_vector_layer/tiled_vector_layer';
import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util';
import { InnerJoin } from '../classes/joins/inner_join';
@@ -38,6 +40,7 @@ import {
DataRequestDescriptor,
DrawState,
Goto,
+ HeatmapLayerDescriptor,
LayerDescriptor,
MapCenter,
MapExtent,
@@ -51,11 +54,13 @@ import { Filter, TimeRange } from '../../../../../src/plugins/data/public';
import { ISource } from '../classes/sources/source';
import { ITMSSource } from '../classes/sources/tms_source';
import { IVectorSource } from '../classes/sources/vector_source';
+import { ESGeoGridSource } from '../classes/sources/es_geo_grid_source';
import { ILayer } from '../classes/layers/layer';
export function createLayerInstance(
layerDescriptor: LayerDescriptor,
- inspectorAdapters?: Adapters
+ inspectorAdapters?: Adapters,
+ chartsPaletteServiceGetColor?: (value: string) => string | null
): ILayer {
const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters);
@@ -75,15 +80,20 @@ export function createLayerInstance(
layerDescriptor: vectorLayerDescriptor,
source: source as IVectorSource,
joins,
+ chartsPaletteServiceGetColor,
});
case VectorTileLayer.type:
return new VectorTileLayer({ layerDescriptor, source: source as ITMSSource });
case HeatmapLayer.type:
- return new HeatmapLayer({ layerDescriptor, source });
+ return new HeatmapLayer({
+ layerDescriptor: layerDescriptor as HeatmapLayerDescriptor,
+ source: source as ESGeoGridSource,
+ });
case BlendedVectorLayer.type:
return new BlendedVectorLayer({
layerDescriptor: layerDescriptor as VectorLayerDescriptor,
source: source as IVectorSource,
+ chartsPaletteServiceGetColor,
});
case TiledVectorLayer.type:
return new TiledVectorLayer({
@@ -295,9 +305,10 @@ export const getSpatialFiltersLayer = createSelector(
export const getLayerList = createSelector(
getLayerListRaw,
getInspectorAdapters,
- (layerDescriptorList, inspectorAdapters) => {
+ getChartsPaletteServiceGetColor,
+ (layerDescriptorList, inspectorAdapters, chartsPaletteServiceGetColor) => {
return layerDescriptorList.map((layerDescriptor) =>
- createLayerInstance(layerDescriptor, inspectorAdapters)
+ createLayerInstance(layerDescriptor, inspectorAdapters, chartsPaletteServiceGetColor)
);
}
);
diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts
index 7440b6ee1e1df..cb22a98b70aa8 100644
--- a/x-pack/plugins/maps/server/plugin.ts
+++ b/x-pack/plugins/maps/server/plugin.ts
@@ -177,6 +177,7 @@ export class MapsPlugin implements Plugin {
catalogue: [APP_ID],
privileges: {
all: {
+ api: ['fileUpload:import'],
app: [APP_ID, 'kibana'],
catalogue: [APP_ID],
savedObject: {
diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json
index b70459c690c07..4a8bfe2ebae66 100644
--- a/x-pack/plugins/maps/tsconfig.json
+++ b/x-pack/plugins/maps/tsconfig.json
@@ -19,7 +19,7 @@
{ "path": "../../../src/plugins/maps_legacy/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
- { "path": "../maps_file_upload/tsconfig.json" },
+ { "path": "../file_upload/tsconfig.json" },
{ "path": "../saved_objects_tagging/tsconfig.json" },
]
}
diff --git a/x-pack/plugins/maps_file_upload/README.md b/x-pack/plugins/maps_file_upload/README.md
deleted file mode 100644
index 1e3343664afb8..0000000000000
--- a/x-pack/plugins/maps_file_upload/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Maps File upload
-
-Deprecated - plugin targeted for removal and will get merged into file_upload plugin
diff --git a/x-pack/plugins/maps_file_upload/common/constants/file_import.ts b/x-pack/plugins/maps_file_upload/common/constants/file_import.ts
deleted file mode 100644
index 9e4763c2c8113..0000000000000
--- a/x-pack/plugins/maps_file_upload/common/constants/file_import.ts
+++ /dev/null
@@ -1,21 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-export const MAX_BYTES = 31457280;
-
-export const MAX_FILE_SIZE = 52428800;
-
-// Value to use in the Elasticsearch index mapping metadata to identify the
-// index as having been created by the File Upload Plugin.
-export const INDEX_META_DATA_CREATED_BY = 'file-upload-plugin';
-
-export const ES_GEO_FIELD_TYPE = {
- GEO_POINT: 'geo_point',
- GEO_SHAPE: 'geo_shape',
-};
-
-export const DEFAULT_KBN_VERSION = 'kbnVersion';
diff --git a/x-pack/plugins/maps_file_upload/jest.config.js b/x-pack/plugins/maps_file_upload/jest.config.js
deleted file mode 100644
index e7b45a559df10..0000000000000
--- a/x-pack/plugins/maps_file_upload/jest.config.js
+++ /dev/null
@@ -1,12 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-module.exports = {
- preset: '@kbn/test',
- rootDir: '../../..',
- roots: ['/x-pack/plugins/maps_file_upload'],
-};
diff --git a/x-pack/plugins/maps_file_upload/kibana.json b/x-pack/plugins/maps_file_upload/kibana.json
deleted file mode 100644
index f544c56cba517..0000000000000
--- a/x-pack/plugins/maps_file_upload/kibana.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id": "mapsFileUpload",
- "version": "8.0.0",
- "kibanaVersion": "kibana",
- "server": true,
- "ui": true,
- "requiredPlugins": ["data", "usageCollection"]
-}
diff --git a/x-pack/plugins/maps_file_upload/mappings.ts b/x-pack/plugins/maps_file_upload/mappings.ts
deleted file mode 100644
index b8b263409f814..0000000000000
--- a/x-pack/plugins/maps_file_upload/mappings.ts
+++ /dev/null
@@ -1,16 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-export const mappings = {
- 'file-upload-telemetry': {
- properties: {
- filesUploadedTotalCount: {
- type: 'long',
- },
- },
- },
-};
diff --git a/x-pack/plugins/maps_file_upload/server/index.js b/x-pack/plugins/maps_file_upload/server/index.js
deleted file mode 100644
index 4bf4e931c7eaa..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/index.js
+++ /dev/null
@@ -1,12 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { FileUploadPlugin } from './plugin';
-
-export * from './plugin';
-
-export const plugin = () => new FileUploadPlugin();
diff --git a/x-pack/plugins/maps_file_upload/server/kibana_server_services.js b/x-pack/plugins/maps_file_upload/server/kibana_server_services.js
deleted file mode 100644
index 8a1278f433ab9..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/kibana_server_services.js
+++ /dev/null
@@ -1,12 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-let internalRepository;
-export const setInternalRepository = (createInternalRepository) => {
- internalRepository = createInternalRepository();
-};
-export const getInternalRepository = () => internalRepository;
diff --git a/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js b/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js
deleted file mode 100644
index 7ba491a8ea49e..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/models/import_data/import_data.js
+++ /dev/null
@@ -1,161 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_import';
-import uuid from 'uuid';
-
-export function importDataProvider(callWithRequest) {
- async function importData(id, index, settings, mappings, ingestPipeline, data) {
- let createdIndex;
- let createdPipelineId;
- const docCount = data.length;
-
- try {
- const { id: pipelineId, pipeline } = ingestPipeline;
-
- if (!id) {
- // first chunk of data, create the index and id to return
- id = uuid.v1();
-
- await createIndex(index, settings, mappings);
- createdIndex = index;
-
- // create the pipeline if one has been supplied
- if (pipelineId !== undefined) {
- const success = await createPipeline(pipelineId, pipeline);
- if (success.acknowledged !== true) {
- throw success;
- }
- }
- createdPipelineId = pipelineId;
- } else {
- createdIndex = index;
- createdPipelineId = pipelineId;
- }
-
- let failures = [];
- if (data.length) {
- const resp = await indexData(index, createdPipelineId, data);
- if (resp.success === false) {
- if (resp.ingestError) {
- // all docs failed, abort
- throw resp;
- } else {
- // some docs failed.
- // still report success but with a list of failures
- failures = resp.failures || [];
- }
- }
- }
-
- return {
- success: true,
- id,
- index: createdIndex,
- pipelineId: createdPipelineId,
- docCount,
- failures,
- };
- } catch (error) {
- return {
- success: false,
- id,
- index: createdIndex,
- pipelineId: createdPipelineId,
- error: error.error !== undefined ? error.error : error,
- docCount,
- ingestError: error.ingestError,
- failures: error.failures || [],
- };
- }
- }
-
- async function createIndex(index, settings, mappings) {
- const body = {
- mappings: {
- _meta: {
- created_by: INDEX_META_DATA_CREATED_BY,
- },
- properties: mappings,
- },
- };
-
- if (settings && Object.keys(settings).length) {
- body.settings = settings;
- }
-
- await callWithRequest('indices.create', { index, body });
- }
-
- async function indexData(index, pipelineId, data) {
- try {
- const body = [];
- for (let i = 0; i < data.length; i++) {
- body.push({ index: {} });
- body.push(data[i]);
- }
-
- const settings = { index, body };
- if (pipelineId !== undefined) {
- settings.pipeline = pipelineId;
- }
-
- const resp = await callWithRequest('bulk', settings);
- if (resp.errors) {
- throw resp;
- } else {
- return {
- success: true,
- docs: data.length,
- failures: [],
- };
- }
- } catch (error) {
- let failures = [];
- let ingestError = false;
- if (error.errors !== undefined && Array.isArray(error.items)) {
- // an expected error where some or all of the bulk request
- // docs have failed to be ingested.
- failures = getFailures(error.items, data);
- } else {
- // some other error has happened.
- ingestError = true;
- }
-
- return {
- success: false,
- error,
- docCount: data.length,
- failures,
- ingestError,
- };
- }
- }
-
- async function createPipeline(id, pipeline) {
- return await callWithRequest('ingest.putPipeline', { id, body: pipeline });
- }
-
- function getFailures(items, data) {
- const failures = [];
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- if (item.index && item.index.error) {
- failures.push({
- item: i,
- reason: item.index.error.reason,
- doc: data[i],
- });
- }
- }
- return failures;
- }
-
- return {
- importData,
- };
-}
diff --git a/x-pack/plugins/maps_file_upload/server/plugin.js b/x-pack/plugins/maps_file_upload/server/plugin.js
deleted file mode 100644
index 1072da863acc7..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/plugin.js
+++ /dev/null
@@ -1,27 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { initRoutes } from './routes/file_upload';
-import { setInternalRepository } from './kibana_server_services';
-import { registerFileUploadUsageCollector, fileUploadTelemetryMappingsType } from './telemetry';
-
-export class FileUploadPlugin {
- constructor() {
- this.router = null;
- }
-
- setup(core, plugins) {
- core.savedObjects.registerType(fileUploadTelemetryMappingsType);
- this.router = core.http.createRouter();
- registerFileUploadUsageCollector(plugins.usageCollection);
- }
-
- start(core) {
- initRoutes(this.router, core.savedObjects.getSavedObjectsRepository);
- setInternalRepository(core.savedObjects.createInternalRepository);
- }
-}
diff --git a/x-pack/plugins/maps_file_upload/server/routes/file_upload.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.js
deleted file mode 100644
index 1b617c44113a2..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/routes/file_upload.js
+++ /dev/null
@@ -1,130 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { importDataProvider } from '../models/import_data';
-import { updateTelemetry } from '../telemetry/telemetry';
-import { MAX_BYTES } from '../../common/constants/file_import';
-import { schema } from '@kbn/config-schema';
-
-export const IMPORT_ROUTE = '/api/maps/fileupload/import';
-
-export const querySchema = schema.maybe(
- schema.object({
- id: schema.nullable(schema.string()),
- })
-);
-
-export const bodySchema = schema.object(
- {
- app: schema.maybe(schema.string()),
- index: schema.string(),
- fileType: schema.string(),
- ingestPipeline: schema.maybe(
- schema.object(
- {},
- {
- defaultValue: {},
- unknowns: 'allow',
- }
- )
- ),
- },
- { unknowns: 'allow' }
-);
-
-const options = {
- body: {
- maxBytes: MAX_BYTES,
- accepts: ['application/json'],
- },
-};
-
-export const idConditionalValidation = (body, boolHasId) =>
- schema
- .object(
- {
- data: boolHasId
- ? schema.arrayOf(schema.object({}, { unknowns: 'allow' }), { minSize: 1 })
- : schema.any(),
- settings: boolHasId
- ? schema.any()
- : schema.object(
- {},
- {
- defaultValue: {
- number_of_shards: 1,
- },
- unknowns: 'allow',
- }
- ),
- mappings: boolHasId
- ? schema.any()
- : schema.object(
- {},
- {
- defaultValue: {},
- unknowns: 'allow',
- }
- ),
- },
- { unknowns: 'allow' }
- )
- .validate(body);
-
-const finishValidationAndProcessReq = () => {
- return async (con, req, { ok, badRequest }) => {
- const {
- query: { id },
- body,
- } = req;
- const boolHasId = !!id;
-
- let resp;
- try {
- const validIdReqData = idConditionalValidation(body, boolHasId);
- const callWithRequest = con.core.elasticsearch.legacy.client.callAsCurrentUser;
- const { importData: importDataFunc } = importDataProvider(callWithRequest);
-
- const { index, settings, mappings, ingestPipeline, data } = validIdReqData;
- const processedReq = await importDataFunc(
- id,
- index,
- settings,
- mappings,
- ingestPipeline,
- data
- );
-
- if (processedReq.success) {
- resp = ok({ body: processedReq });
- // If no id's been established then this is a new index, update telemetry
- if (!boolHasId) {
- await updateTelemetry();
- }
- } else {
- resp = badRequest(`Error processing request 1: ${processedReq.error.message}`, ['body']);
- }
- } catch (e) {
- resp = badRequest(`Error processing request 2: : ${e.message}`, ['body']);
- }
- return resp;
- };
-};
-
-export const initRoutes = (router) => {
- router.post(
- {
- path: `${IMPORT_ROUTE}{id?}`,
- validate: {
- query: querySchema,
- body: bodySchema,
- },
- options,
- },
- finishValidationAndProcessReq()
- );
-};
diff --git a/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js b/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js
deleted file mode 100644
index e893e103aad72..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/routes/file_upload.test.js
+++ /dev/null
@@ -1,77 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { querySchema, bodySchema, idConditionalValidation } from './file_upload';
-
-const queryWithId = {
- id: '123',
-};
-
-const bodyWithoutQueryId = {
- index: 'islandofone',
- data: [],
- settings: { number_of_shards: 1 },
- mappings: { coordinates: { type: 'geo_point' } },
- ingestPipeline: {},
- fileType: 'json',
- app: 'Maps',
-};
-
-const bodyWithQueryId = {
- index: 'islandofone2',
- data: [{ coordinates: [], name: 'islandofone2' }],
- settings: {},
- mappings: {},
- ingestPipeline: {},
- fileType: 'json',
-};
-
-describe('route validation', () => {
- it(`validates query with id`, async () => {
- const validationResult = querySchema.validate(queryWithId);
- expect(validationResult.id).toBe(queryWithId.id);
- });
-
- it(`validates query without id`, async () => {
- const validationResult = querySchema.validate({});
- expect(validationResult.id).toBeNull();
- });
-
- it(`throws when query contains content other than an id`, async () => {
- expect(() => querySchema.validate({ notAnId: 123 })).toThrowError(
- `[notAnId]: definition for this key is missing`
- );
- });
-
- it(`validates body with valid fields`, async () => {
- const validationResult = bodySchema.validate(bodyWithoutQueryId);
- expect(validationResult).toEqual(bodyWithoutQueryId);
- });
-
- it(`throws if an expected field is missing`, async () => {
- /* eslint-disable no-unused-vars */
- const { index, ...bodyWithoutIndexField } = bodyWithoutQueryId;
- expect(() => bodySchema.validate(bodyWithoutIndexField)).toThrowError(
- `[index]: expected value of type [string] but got [undefined]`
- );
- });
-
- it(`validates conditional fields when id has been provided in query`, async () => {
- const validationResult = idConditionalValidation(bodyWithQueryId, true);
- expect(validationResult).toEqual(bodyWithQueryId);
- });
-
- it(`validates conditional fields when no id has been provided in query`, async () => {
- const validationResultWhenIdPresent = idConditionalValidation(bodyWithoutQueryId, false);
- expect(validationResultWhenIdPresent).toEqual(bodyWithoutQueryId);
- // Conditions for no id are more strict since this query sets up the index,
- // expect it to throw if expected fields aren't present
- expect(() => idConditionalValidation(bodyWithoutQueryId, true)).toThrowError(
- `[data]: array size is [0], but cannot be smaller than [1]`
- );
- });
-});
diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts
deleted file mode 100644
index bf786aa830448..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/telemetry/file_upload_usage_collector.ts
+++ /dev/null
@@ -1,29 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { getTelemetry, initTelemetry, Telemetry } from './telemetry';
-
-export function registerFileUploadUsageCollector(usageCollection: UsageCollectionSetup): void {
- const fileUploadUsageCollector = usageCollection.makeUsageCollector({
- type: 'fileUploadTelemetry',
- isReady: () => true,
- fetch: async () => {
- const fileUploadUsage = await getTelemetry();
- if (!fileUploadUsage) {
- return initTelemetry();
- }
-
- return fileUploadUsage;
- },
- schema: {
- filesUploadedTotalCount: { type: 'long' },
- },
- });
-
- usageCollection.registerCollector(fileUploadUsageCollector);
-}
diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts b/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts
deleted file mode 100644
index ee79e2f6c6d47..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/telemetry/mappings.ts
+++ /dev/null
@@ -1,22 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { SavedObjectsType } from 'src/core/server';
-import { TELEMETRY_DOC_ID } from './telemetry';
-
-export const fileUploadTelemetryMappingsType: SavedObjectsType = {
- name: TELEMETRY_DOC_ID,
- hidden: false,
- namespaceType: 'agnostic',
- mappings: {
- properties: {
- filesUploadedTotalCount: {
- type: 'long',
- },
- },
- },
-};
diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts b/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts
deleted file mode 100644
index 2ca01b03aa633..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.test.ts
+++ /dev/null
@@ -1,48 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { getTelemetry, updateTelemetry } from './telemetry';
-
-const internalRepository = () => ({
- get: jest.fn(() => null),
- create: jest.fn(() => ({ attributes: 'test' })),
- update: jest.fn(() => ({ attributes: 'test' })),
-});
-
-function mockInit(getVal: any = { attributes: {} }): any {
- return {
- ...internalRepository(),
- get: jest.fn(() => getVal),
- };
-}
-
-describe('file upload plugin telemetry', () => {
- describe('getTelemetry', () => {
- it('should get existing telemetry', async () => {
- const internalRepo = mockInit();
- await getTelemetry(internalRepo);
- expect(internalRepo.update.mock.calls.length).toBe(0);
- expect(internalRepo.get.mock.calls.length).toBe(1);
- expect(internalRepo.create.mock.calls.length).toBe(0);
- });
- });
-
- describe('updateTelemetry', () => {
- it('should update existing telemetry', async () => {
- const internalRepo = mockInit({
- attributes: {
- filesUploadedTotalCount: 2,
- },
- });
-
- await updateTelemetry(internalRepo);
- expect(internalRepo.update.mock.calls.length).toBe(1);
- expect(internalRepo.get.mock.calls.length).toBe(1);
- expect(internalRepo.create.mock.calls.length).toBe(0);
- });
- });
-});
diff --git a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts b/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts
deleted file mode 100644
index 0e53c2570e01b..0000000000000
--- a/x-pack/plugins/maps_file_upload/server/telemetry/telemetry.ts
+++ /dev/null
@@ -1,62 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import _ from 'lodash';
-// @ts-ignore
-import { getInternalRepository } from '../kibana_server_services';
-
-export const TELEMETRY_DOC_ID = 'file-upload-telemetry';
-
-export interface Telemetry {
- filesUploadedTotalCount: number;
-}
-
-export interface TelemetrySavedObject {
- attributes: Telemetry;
-}
-
-export function initTelemetry(): Telemetry {
- return {
- filesUploadedTotalCount: 0,
- };
-}
-
-export async function getTelemetry(internalRepo?: object): Promise {
- const internalRepository = internalRepo || getInternalRepository();
- let telemetrySavedObject;
-
- try {
- telemetrySavedObject = await internalRepository.get(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID);
- } catch (e) {
- // Fail silently
- }
-
- return telemetrySavedObject ? telemetrySavedObject.attributes : null;
-}
-
-export async function updateTelemetry(internalRepo?: any) {
- const internalRepository = internalRepo || getInternalRepository();
- let telemetry = await getTelemetry(internalRepository);
- // Create if doesn't exist
- if (!telemetry || _.isEmpty(telemetry)) {
- const newTelemetrySavedObject = await internalRepository.create(
- TELEMETRY_DOC_ID,
- initTelemetry(),
- { id: TELEMETRY_DOC_ID }
- );
- telemetry = newTelemetrySavedObject.attributes;
- }
-
- await internalRepository.update(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID, incrementCounts(telemetry));
-}
-
-export function incrementCounts({ filesUploadedTotalCount }: { filesUploadedTotalCount: number }) {
- return {
- // TODO: get telemetry for app, total file counts, file type
- filesUploadedTotalCount: filesUploadedTotalCount + 1,
- };
-}
diff --git a/x-pack/plugins/maps_file_upload/tsconfig.json b/x-pack/plugins/maps_file_upload/tsconfig.json
deleted file mode 100644
index f068d62b71739..0000000000000
--- a/x-pack/plugins/maps_file_upload/tsconfig.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "extends": "../../../tsconfig.base.json",
- "compilerOptions": {
- "composite": true,
- "outDir": "./target/types",
- "emitDeclarationOnly": true,
- "declaration": true,
- "declarationMap": true
- },
- "include": ["common/**/*", "public/**/*", "server/**/*", "mappings.ts"],
- "references": [
- { "path": "../../../src/plugins/data/tsconfig.json" },
- { "path": "../../../src/plugins/usage_collection/tsconfig.json" }
- ]
-}
diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts
index eb7615c79a363..974a1f2243060 100644
--- a/x-pack/plugins/ml/common/types/capabilities.ts
+++ b/x-pack/plugins/ml/common/types/capabilities.ts
@@ -99,7 +99,7 @@ export function getPluginPrivileges() {
return {
admin: {
...privilege,
- api: allMlCapabilitiesKeys.map((k) => `ml:${k}`),
+ api: ['fileUpload:import', ...allMlCapabilitiesKeys.map((k) => `ml:${k}`)],
catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`],
ui: allMlCapabilitiesKeys,
savedObject: {
diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts
index faa9c700f95a4..7c9623d3e68ec 100644
--- a/x-pack/plugins/ml/common/types/modules.ts
+++ b/x-pack/plugins/ml/common/types/modules.ts
@@ -68,6 +68,7 @@ export interface KibanaObjectResponse extends ResultItem {
export interface DatafeedResponse extends ResultItem {
started: boolean;
+ awaitingMlNodeAllocation?: boolean;
error?: ErrorType;
}
diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx
index bc216ce62a57c..2cc36b7a2adf7 100644
--- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx
+++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx
@@ -9,14 +9,14 @@ import React, { Fragment, FC } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { isCloud } from '../../services/ml_server_info';
+import { lazyMlNodesAvailable } from '../../ml_nodes_check';
interface Props {
jobCount: number;
}
export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => {
- if (isCloud() === false || jobCount === 0) {
+ if (lazyMlNodesAvailable() === false || jobCount === 0) {
return null;
}
@@ -26,7 +26,7 @@ export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => {
title={
}
color="primary"
@@ -35,7 +35,7 @@ export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => {
= () => {
+ if (lazyMlNodesAvailable() === false) {
+ return null;
+ }
+
return (
}
color="primary"
@@ -31,7 +36,7 @@ export const NewJobAwaitingNodeWarning: FC = () => {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
index 039a00afe52ee..ee66612de97ac 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
@@ -117,6 +117,10 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo
optional: true,
defaultValue: 'maximize_minimum_recall',
},
+ early_stopping_enabled: {
+ optional: true,
+ ignore: true,
+ },
},
}
: {}),
@@ -207,6 +211,10 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo
loss_function_parameter: {
optional: true,
},
+ early_stopping_enabled: {
+ optional: true,
+ ignore: true,
+ },
},
}
: {}),
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
index 8de4470b028f5..a70962c45ffcb 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
@@ -56,6 +56,7 @@ export interface State {
destinationIndexNameEmpty: boolean;
destinationIndexNameValid: boolean;
destinationIndexPatternTitleExists: boolean;
+ earlyStoppingEnabled: undefined | boolean;
eta: undefined | number;
featureBagFraction: undefined | number;
featureInfluenceThreshold: undefined | number;
@@ -125,6 +126,7 @@ export const getInitialState = (): State => ({
destinationIndexNameEmpty: true,
destinationIndexNameValid: false,
destinationIndexPatternTitleExists: false,
+ earlyStoppingEnabled: undefined,
eta: undefined,
featureBagFraction: undefined,
featureInfluenceThreshold: undefined,
@@ -239,7 +241,10 @@ export const getJobConfigFromFormState = (
formState.gamma && { gamma: formState.gamma },
formState.lambda && { lambda: formState.lambda },
formState.maxTrees && { max_trees: formState.maxTrees },
- formState.randomizeSeed && { randomize_seed: formState.randomizeSeed }
+ formState.randomizeSeed && { randomize_seed: formState.randomizeSeed },
+ formState.earlyStoppingEnabled !== undefined && {
+ early_stopping_enabled: formState.earlyStoppingEnabled,
+ }
);
jobConfig.analysis = {
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx
index 0535b15912a9b..0fa7de4732c39 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.tsx
@@ -12,7 +12,7 @@ import { EuiCallOut, EuiSpacer, EuiButtonEmpty, EuiHorizontalRule } from '@elast
import numeral from '@elastic/numeral';
import { ErrorResponse } from '../../../../../../common/types/errors';
-import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../../../file_upload/common';
+import { FILE_SIZE_DISPLAY_FORMAT } from '../../../../../../../file_upload/public';
interface FileTooLargeProps {
fileSize: number;
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts
index 47f262ef45a18..4412390d62c1f 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts
@@ -15,7 +15,7 @@ import {
MAX_FILE_SIZE_BYTES,
ABSOLUTE_MAX_FILE_SIZE_BYTES,
FILE_SIZE_DISPLAY_FORMAT,
-} from '../../../../../../../file_upload/common';
+} from '../../../../../../../file_upload/public';
import { getUiSettings } from '../../../../util/dependency_cache';
import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../../../../../common/constants/settings';
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx
index 760ff67d97b9d..311e291cf2519 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx
@@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { ModuleJobUI } from '../page';
import { SETUP_RESULTS_WIDTH } from './module_jobs';
import { tabColor } from '../../../../../../common/util/group_color_utils';
-import { JobOverride } from '../../../../../../common/types/modules';
+import { JobOverride, DatafeedResponse } from '../../../../../../common/types/modules';
import { extractErrorMessage } from '../../../../../../common/util/errors';
interface JobItemProps {
@@ -151,8 +151,8 @@ export const JobItem: FC = memo(
= memo(
);
}
);
+
+function getDatafeedStartedIcon({
+ awaitingMlNodeAllocation,
+ success,
+}: DatafeedResponse): { type: string; color: string } {
+ if (awaitingMlNodeAllocation === true) {
+ return { type: 'alert', color: 'warning' };
+ }
+
+ return success ? { type: 'check', color: 'secondary' } : { type: 'cross', color: 'danger' };
+}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx
index 14018d485e04c..271898654ca49 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx
@@ -43,6 +43,7 @@ import { TimeRange } from '../common/components';
import { JobId } from '../../../../../common/types/anomaly_detection_jobs';
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { TIME_FORMAT } from '../../../../../common/constants/time_format';
+import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning';
export interface ModuleJobUI extends ModuleJob {
datafeedResult?: DatafeedResponse;
@@ -84,6 +85,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => {
const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED);
const [resultsUrl, setResultsUrl] = useState('');
const [existingGroups, setExistingGroups] = useState(existingGroupIds);
+ const [jobsAwaitingNodeCount, setJobsAwaitingNodeCount] = useState(0);
// #endregion
const {
@@ -204,9 +206,19 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => {
});
setResultsUrl(url);
- const failedJobsCount = jobsResponse.reduce((count, { success }) => {
- return success ? count : count + 1;
- }, 0);
+ const failedJobsCount = jobsResponse.reduce(
+ (count, { success }) => (success ? count : count + 1),
+ 0
+ );
+
+ const lazyJobsCount = datafeedsResponse.reduce(
+ (count, { awaitingMlNodeAllocation }) =>
+ awaitingMlNodeAllocation === true ? count + 1 : count,
+ 0
+ );
+
+ setJobsAwaitingNodeCount(lazyJobsCount);
+
setSaveState(
failedJobsCount === 0
? SAVE_STATE.SAVED
@@ -291,6 +303,8 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => {
>
)}
+ {jobsAwaitingNodeCount > 0 && }
+
diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
index 71aef2da312a6..551a5823c1f41 100644
--- a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
+++ b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
@@ -48,6 +48,14 @@ export function mlNodesAvailable() {
return mlNodeCount !== 0 || lazyMlNodeCount !== 0;
}
+export function currentMlNodesAvailable() {
+ return mlNodeCount !== 0;
+}
+
+export function lazyMlNodesAvailable() {
+ return lazyMlNodeCount !== 0;
+}
+
export function permissionToViewMlNodeCount() {
return userHasPermissionToViewMlNodeCount;
}
diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts
index 295ff1aca2ec7..8102f95c035b0 100644
--- a/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts
+++ b/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts
@@ -9,5 +9,6 @@ export {
checkMlNodesAvailable,
getMlNodeCount,
mlNodesAvailable,
+ lazyMlNodesAvailable,
permissionToViewMlNodeCount,
} from './check_ml_nodes';
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js
index a37ad5fd30517..b36acba8b4ba4 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js
@@ -27,7 +27,7 @@ import {
import { JOB_STATE } from '../../../../../common/constants/states';
import { FORECAST_DURATION_MAX_DAYS } from './forecasting_modal';
import { ForecastProgress } from './forecast_progress';
-import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
+import { currentMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
import {
checkPermission,
createPermissionFailureMessage,
@@ -41,7 +41,7 @@ function getRunInputDisabledState(job, isForecastRequested) {
// - No canForecastJob permission
// - Job is not in an OPENED or CLOSED state
// - A new forecast has been requested
- if (mlNodesAvailable() === false) {
+ if (currentMlNodesAvailable() === false) {
return {
isDisabled: true,
isDisabledToolTipText: i18n.translate(
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
index 0d75db64a01b9..fa0cccda99d22 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
@@ -36,7 +36,7 @@ import { parseInterval } from '../../../common/util/parse_interval';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container';
import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
-import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/apply_influencer_filters_action';
+import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/constants';
import {
AnomalySwimlaneEmbeddableInput,
AnomalySwimlaneEmbeddableOutput,
diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts
index 1c4aa4031171d..c88ce2d7f95d2 100755
--- a/x-pack/plugins/ml/public/index.ts
+++ b/x-pack/plugins/ml/public/index.ts
@@ -39,8 +39,18 @@ export type {
RenderCellValue,
} from './shared';
+export type { AnomalySwimlaneEmbeddableInput } from './embeddables';
+
+export { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from './embeddables/constants';
+export { CONTROLLED_BY_SWIM_LANE_FILTER } from './ui_actions/constants';
+
// Static exports
-export { getSeverityColor, getSeverityType } from '../common/util/anomaly_utils';
+export {
+ getSeverityColor,
+ getSeverityType,
+ getFormattedSeverityScore,
+} from '../common/util/anomaly_utils';
+
export { ANOMALY_SEVERITY } from '../common';
export { useMlHref, ML_PAGES, MlUrlGenerator } from './ml_url_generator';
diff --git a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx
index e9b70ee14aae6..e3d2ca4ce0de1 100644
--- a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx
+++ b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx
@@ -11,11 +11,10 @@ import { MlCoreSetup } from '../plugin';
import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from '../application/explorer/explorer_constants';
import { Filter, FilterStateStore } from '../../../../../src/plugins/data/common';
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables';
+import { CONTROLLED_BY_SWIM_LANE_FILTER } from './constants';
export const APPLY_INFLUENCER_FILTERS_ACTION = 'applyInfluencerFiltersAction';
-export const CONTROLLED_BY_SWIM_LANE_FILTER = 'anomaly-swim-lane';
-
export function createApplyInfluencerFiltersAction(
getStartServices: MlCoreSetup['getStartServices']
) {
diff --git a/x-pack/jest.config.js b/x-pack/plugins/ml/public/ui_actions/constants.ts
similarity index 67%
rename from x-pack/jest.config.js
rename to x-pack/plugins/ml/public/ui_actions/constants.ts
index 231004359632b..6dc3f03d10fd9 100644
--- a/x-pack/jest.config.js
+++ b/x-pack/plugins/ml/public/ui_actions/constants.ts
@@ -5,8 +5,4 @@
* 2.0.
*/
-module.exports = {
- preset: '@kbn/test',
- rootDir: '..',
- projects: ['/x-pack/plugins/*/jest.config.js'],
-};
+export const CONTROLLED_BY_SWIM_LANE_FILTER = 'anomaly-swim-lane';
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts
index 92dfe3aa0fbf9..a1fac92d45b4e 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts
+++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts
@@ -491,6 +491,7 @@ export class DataRecognizer {
const startedDatafeed = startResults[df.id];
if (startedDatafeed !== undefined) {
df.started = startedDatafeed.started;
+ df.awaitingMlNodeAllocation = startedDatafeed.awaitingMlNodeAllocation;
if (startedDatafeed.error !== undefined) {
df.error = startedDatafeed.error;
}
@@ -749,9 +750,20 @@ export class DataRecognizer {
datafeeds.map(async (datafeed) => {
try {
await this.saveDatafeed(datafeed);
- return { id: datafeed.id, success: true, started: false };
+ return {
+ id: datafeed.id,
+ success: true,
+ started: false,
+ awaitingMlNodeAllocation: false,
+ };
} catch ({ body }) {
- return { id: datafeed.id, success: false, started: false, error: body };
+ return {
+ id: datafeed.id,
+ success: false,
+ started: false,
+ awaitingMlNodeAllocation: false,
+ error: body,
+ };
}
})
);
@@ -811,11 +823,18 @@ export class DataRecognizer {
duration.end = (end as unknown) as string;
}
- await this._mlClient.startDatafeed({
+ const {
+ body: { started, node },
+ } = await this._mlClient.startDatafeed<{
+ started: boolean;
+ node: string;
+ }>({
datafeed_id: datafeed.id,
...duration,
});
- result.started = true;
+
+ result.started = started;
+ result.awaitingMlNodeAllocation = node?.length === 0;
} catch ({ body }) {
result.started = false;
result.error = body;
@@ -845,6 +864,7 @@ export class DataRecognizer {
if (d.id === d2.id) {
d.success = d2.success;
d.started = d2.started;
+ d.awaitingMlNodeAllocation = d2.awaitingMlNodeAllocation;
if (d2.error !== undefined) {
d.error = d2.error;
}
diff --git a/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts b/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts
index 454379d17848e..8e9e8d916e496 100644
--- a/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts
+++ b/x-pack/plugins/monitoring/server/es_client/monitoring_endpoint_disable_watches.ts
@@ -13,7 +13,7 @@ export function monitoringEndpointDisableWatches(Client: any, _config: any, comp
params: {},
urls: [
{
- fmt: '_monitoring/migrate/alerts',
+ fmt: '/_monitoring/migrate/alerts',
},
],
method: 'POST',
diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts
index 066a2d56cbeec..9348fd1eb20df 100644
--- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts
+++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts
@@ -10,13 +10,26 @@ import { register } from './add_route';
import { API_BASE_PATH } from '../../../common/constants';
import { LicenseStatus } from '../../types';
-import { xpackMocks } from '../../../../../mocks';
+import { licensingMock } from '../../../../../plugins/licensing/server/mocks';
+
import {
elasticsearchServiceMock,
httpServerMock,
httpServiceMock,
+ coreMock,
} from '../../../../../../src/core/server/mocks';
+// Re-implement the mock that was imported directly from `x-pack/mocks`
+function createCoreRequestHandlerContextMock() {
+ return {
+ core: coreMock.createRequestHandlerContext(),
+ licensing: licensingMock.createRequestHandlerContext(),
+ };
+}
+
+const xpackMocks = {
+ createRequestHandlerContext: createCoreRequestHandlerContextMock,
+};
interface TestOptions {
licenseCheckResult?: LicenseStatus;
apiResponses?: Array<() => Promise>;
diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts
index 29d846314bd9b..ce94f45bb8443 100644
--- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts
+++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts
@@ -10,13 +10,26 @@ import { register } from './delete_route';
import { API_BASE_PATH } from '../../../common/constants';
import { LicenseStatus } from '../../types';
-import { xpackMocks } from '../../../../../mocks';
+import { licensingMock } from '../../../../../plugins/licensing/server/mocks';
+
import {
elasticsearchServiceMock,
httpServerMock,
httpServiceMock,
+ coreMock,
} from '../../../../../../src/core/server/mocks';
+// Re-implement the mock that was imported directly from `x-pack/mocks`
+function createCoreRequestHandlerContextMock() {
+ return {
+ core: coreMock.createRequestHandlerContext(),
+ licensing: licensingMock.createRequestHandlerContext(),
+ };
+}
+
+const xpackMocks = {
+ createRequestHandlerContext: createCoreRequestHandlerContextMock,
+};
interface TestOptions {
licenseCheckResult?: LicenseStatus;
apiResponses?: Array<() => Promise>;
diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts
index 33a3142ddc105..25d17d796b0ee 100644
--- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts
+++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts
@@ -12,13 +12,26 @@ import { register } from './get_route';
import { API_BASE_PATH } from '../../../common/constants';
import { LicenseStatus } from '../../types';
-import { xpackMocks } from '../../../../../mocks';
+import { licensingMock } from '../../../../../plugins/licensing/server/mocks';
+
import {
elasticsearchServiceMock,
httpServerMock,
httpServiceMock,
+ coreMock,
} from '../../../../../../src/core/server/mocks';
+// Re-implement the mock that was imported directly from `x-pack/mocks`
+function createCoreRequestHandlerContextMock() {
+ return {
+ core: coreMock.createRequestHandlerContext(),
+ licensing: licensingMock.createRequestHandlerContext(),
+ };
+}
+
+const xpackMocks = {
+ createRequestHandlerContext: createCoreRequestHandlerContextMock,
+};
interface TestOptions {
licenseCheckResult?: LicenseStatus;
apiResponses?: Array<() => Promise>;
diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts
index 31db362f7c953..22c87786a585c 100644
--- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts
+++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts
@@ -10,13 +10,26 @@ import { register } from './update_route';
import { API_BASE_PATH } from '../../../common/constants';
import { LicenseStatus } from '../../types';
-import { xpackMocks } from '../../../../../mocks';
+import { licensingMock } from '../../../../../plugins/licensing/server/mocks';
+
import {
elasticsearchServiceMock,
httpServerMock,
httpServiceMock,
+ coreMock,
} from '../../../../../../src/core/server/mocks';
+// Re-implement the mock that was imported directly from `x-pack/mocks`
+function createCoreRequestHandlerContextMock() {
+ return {
+ core: coreMock.createRequestHandlerContext(),
+ licensing: licensingMock.createRequestHandlerContext(),
+ };
+}
+
+const xpackMocks = {
+ createRequestHandlerContext: createCoreRequestHandlerContextMock,
+};
interface TestOptions {
licenseCheckResult?: LicenseStatus;
apiResponses?: Array<() => Promise>;
diff --git a/x-pack/plugins/remote_clusters/tsconfig.json b/x-pack/plugins/remote_clusters/tsconfig.json
new file mode 100644
index 0000000000000..0bee6300cf0b2
--- /dev/null
+++ b/x-pack/plugins/remote_clusters/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "common/**/*",
+ "fixtures/**/*",
+ "public/**/*",
+ "server/**/*",
+ ],
+ "references": [
+ { "path": "../../../src/core/tsconfig.json" },
+ // required plugins
+ { "path": "../licensing/tsconfig.json" },
+ { "path": "../../../src/plugins/management/tsconfig.json" },
+ { "path": "../index_management/tsconfig.json" },
+ { "path": "../features/tsconfig.json" },
+ // optional plugins
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../cloud/tsconfig.json" },
+ // required bundles
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
+ ]
+}
diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json
new file mode 100644
index 0000000000000..9b994d1710ffc
--- /dev/null
+++ b/x-pack/plugins/rollup/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": [
+ "common/**/*",
+ "fixtures/**/*",
+ "public/**/*",
+ "server/**/*",
+ ],
+ "references": [
+ { "path": "../../../src/core/tsconfig.json" },
+ // required plugins
+ { "path": "../../../src/plugins/index_pattern_management/tsconfig.json" },
+ { "path": "../../../src/plugins/management/tsconfig.json" },
+ { "path": "../licensing/tsconfig.json" },
+ { "path": "../features/tsconfig.json" },
+ // optional plugins
+ { "path": "../../../src/plugins/home/tsconfig.json" },
+ { "path": "../index_management/tsconfig.json" },
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" },
+ // required bundles
+ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
+ { "path": "../../../src/plugins/data/tsconfig.json" },
+
+ ]
+}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx
index 13b9c9ef4f519..1616c5e84247f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiModalBody, EuiModalHeader } from '@elastic/eui';
+import { EuiModalBody, EuiModalHeader, EuiSpacer } from '@elastic/eui';
import React, { Fragment, memo, useMemo } from 'react';
import styled from 'styled-components';
@@ -62,11 +62,10 @@ export const OpenTimelineModalBody = memo(
const SearchRowContent = useMemo(
() => (
- {!!timelineFilter && timelineFilter}
{!!templateTimelineFilter && templateTimelineFilter}
),
- [timelineFilter, templateTimelineFilter]
+ [templateTimelineFilter]
);
return (
@@ -84,9 +83,14 @@ export const OpenTimelineModalBody = memo(
<>
+ {!!timelineFilter && (
+ <>
+ {timelineFilter}
+
+ >
+ )}
& {
*/
export const TitleRow = React.memo(
({ children, onAddTimelinesToFavorites, selectedTimelinesCount, title }) => (
-
+
{onAddTimelinesToFavorites && (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
index 84907c74cdace..ae743ad30eef1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
@@ -146,7 +146,7 @@ export const OPEN_TIMELINE = i18n.translate(
export const OPEN_TIMELINE_TITLE = i18n.translate(
'xpack.securitySolution.open.timeline.openTimelineTitle',
{
- defaultMessage: 'Open Timeline',
+ defaultMessage: 'Open',
}
);
@@ -274,12 +274,6 @@ export const SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES = (totalTimelineTemplates:
}
);
-export const FILTER_TIMELINES = (timelineType: string) =>
- i18n.translate('xpack.securitySolution.open.timeline.filterByTimelineTypesTitle', {
- values: { timelineType },
- defaultMessage: 'Only {timelineType}',
- });
-
export const TAB_TIMELINES = i18n.translate(
'xpack.securitySolution.timelines.components.tabs.timelinesTitle',
{
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
index ddf567edafe13..ad62bda4c9783 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
@@ -221,13 +221,11 @@ export enum TimelineTabsStyle {
}
export interface TimelineTab {
- count: number | undefined;
disabled: boolean;
href: string;
id: TimelineTypeLiteral;
name: string;
onClick: (ev: { preventDefault: () => void }) => void;
- withNext: boolean;
}
export interface TemplateTimelineFilter {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx
new file mode 100644
index 0000000000000..1d39dd169ffaa
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx
@@ -0,0 +1,193 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+import { renderHook, act } from '@testing-library/react-hooks';
+import {
+ useTimelineTypes,
+ UseTimelineTypesArgs,
+ UseTimelineTypesResult,
+} from './use_timeline_types';
+
+jest.mock('react-router-dom', () => {
+ return {
+ useParams: jest.fn().mockReturnValue('default'),
+ useHistory: jest.fn().mockReturnValue([]),
+ };
+});
+
+jest.mock('../../../common/components/link_to', () => {
+ return {
+ getTimelineTabsUrl: jest.fn(),
+ useFormatUrl: jest.fn().mockReturnValue({
+ formatUrl: jest.fn(),
+ search: '',
+ }),
+ };
+});
+
+describe('useTimelineTypes', () => {
+ it('init', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook<
+ UseTimelineTypesArgs,
+ UseTimelineTypesResult
+ >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }));
+ await waitForNextUpdate();
+ expect(result.current).toEqual({
+ timelineType: 'default',
+ timelineTabs: result.current.timelineTabs,
+ timelineFilters: result.current.timelineFilters,
+ });
+ });
+ });
+
+ describe('timelineTabs', () => {
+ it('render timelineTabs', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook<
+ UseTimelineTypesArgs,
+ UseTimelineTypesResult
+ >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }));
+ await waitForNextUpdate();
+
+ const { container } = render(result.current.timelineTabs);
+ expect(
+ container.querySelector('[data-test-subj="timeline-tab-default"]')
+ ).toHaveTextContent('Timelines');
+ expect(
+ container.querySelector('[data-test-subj="timeline-tab-template"]')
+ ).toHaveTextContent('Templates');
+ });
+ });
+
+ it('set timelineTypes correctly', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook<
+ UseTimelineTypesArgs,
+ UseTimelineTypesResult
+ >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }));
+ await waitForNextUpdate();
+
+ const { container } = render(result.current.timelineTabs);
+
+ fireEvent(
+ container.querySelector('[data-test-subj="timeline-tab-template"]')!,
+ new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+
+ expect(result.current).toEqual({
+ timelineType: 'template',
+ timelineTabs: result.current.timelineTabs,
+ timelineFilters: result.current.timelineFilters,
+ });
+ });
+ });
+
+ it('stays in the same tab if clicking again on current tab', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook<
+ UseTimelineTypesArgs,
+ UseTimelineTypesResult
+ >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }));
+ await waitForNextUpdate();
+
+ const { container } = render(result.current.timelineTabs);
+
+ fireEvent(
+ container.querySelector('[data-test-subj="timeline-tab-default"]')!,
+ new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+
+ expect(result.current).toEqual({
+ timelineType: 'default',
+ timelineTabs: result.current.timelineTabs,
+ timelineFilters: result.current.timelineFilters,
+ });
+ });
+ });
+ });
+
+ describe('timelineFilters', () => {
+ it('render timelineFilters', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook<
+ UseTimelineTypesArgs,
+ UseTimelineTypesResult
+ >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }));
+ await waitForNextUpdate();
+
+ const { container } = render(<>{result.current.timelineFilters}>);
+ expect(
+ container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]')
+ ).toHaveTextContent('Timelines');
+ expect(
+ container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]')
+ ).toHaveTextContent('Templates');
+ });
+ });
+
+ it('set timelineTypes correctly', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook<
+ UseTimelineTypesArgs,
+ UseTimelineTypesResult
+ >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }));
+ await waitForNextUpdate();
+
+ const { container } = render(<>{result.current.timelineFilters}>);
+
+ fireEvent(
+ container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]')!,
+ new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+
+ expect(result.current).toEqual({
+ timelineType: 'template',
+ timelineTabs: result.current.timelineTabs,
+ timelineFilters: result.current.timelineFilters,
+ });
+ });
+ });
+
+ it('stays in the same tab if clicking again on current tab', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook<
+ UseTimelineTypesArgs,
+ UseTimelineTypesResult
+ >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 }));
+ await waitForNextUpdate();
+
+ const { container } = render(<>{result.current.timelineFilters}>);
+
+ fireEvent(
+ container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]')!,
+ new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+
+ expect(result.current).toEqual({
+ timelineType: 'default',
+ timelineTabs: result.current.timelineTabs,
+ timelineFilters: result.current.timelineFilters,
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
index 728d8b6eeb488..a66fe43d305f1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
@@ -7,7 +7,7 @@
import React, { useState, useCallback, useMemo } from 'react';
import { useParams, useHistory } from 'react-router-dom';
-import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui';
+import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
import { noop } from 'lodash/fp';
import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline';
@@ -24,7 +24,7 @@ export interface UseTimelineTypesArgs {
export interface UseTimelineTypesResult {
timelineType: TimelineTypeLiteralWithNull;
timelineTabs: JSX.Element;
- timelineFilters: JSX.Element[];
+ timelineFilters: JSX.Element;
}
export const useTimelineTypes = ({
@@ -59,51 +59,28 @@ export const useTimelineTypes = ({
(timelineTabsStyle: TimelineTabsStyle) => [
{
id: TimelineType.default,
- name:
- timelineTabsStyle === TimelineTabsStyle.filter
- ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES)
- : i18n.TAB_TIMELINES,
+ name: i18n.TAB_TIMELINES,
href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)),
disabled: false,
- withNext: true,
- count:
- timelineTabsStyle === TimelineTabsStyle.filter
- ? defaultTimelineCount ?? undefined
- : undefined,
+
onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop,
},
{
id: TimelineType.template,
- name:
- timelineTabsStyle === TimelineTabsStyle.filter
- ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES)
- : i18n.TAB_TEMPLATES,
+ name: i18n.TAB_TEMPLATES,
href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)),
disabled: false,
- withNext: false,
- count:
- timelineTabsStyle === TimelineTabsStyle.filter
- ? templateTimelineCount ?? undefined
- : undefined,
+
onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop,
},
],
- [
- defaultTimelineCount,
- templateTimelineCount,
- urlSearch,
- formatUrl,
- goToTimeline,
- goToTemplateTimeline,
- ]
+ [urlSearch, formatUrl, goToTimeline, goToTemplateTimeline]
);
const onFilterClicked = useCallback(
(tabId, tabStyle: TimelineTabsStyle) => {
setTimelineTypes((prevTimelineTypes) => {
- if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) {
- return tabId === TimelineType.default ? TimelineType.template : TimelineType.default;
- } else if (prevTimelineTypes !== tabId) {
+ if (prevTimelineTypes !== tabId) {
setTimelineTypes(tabId);
}
return prevTimelineTypes;
@@ -139,21 +116,23 @@ export const useTimelineTypes = ({
}, [tabName]);
const timelineFilters = useMemo(() => {
- return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => (
- void }) => {
- tab.onClick(ev);
- onFilterClicked(tab.id, TimelineTabsStyle.filter);
- }}
- withNext={tab.withNext}
- >
- {tab.name}
-
- ));
+ return (
+
+ {getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => (
+ void }) => {
+ tab.onClick(ev);
+ onFilterClicked(tab.id, TimelineTabsStyle.filter);
+ }}
+ >
+ {tab.name}
+
+ ))}
+
+ );
}, [timelineType, getFilterOrTabs, onFilterClicked]);
return {
diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts
index 86133d93e99c8..1f49ac7bf5019 100644
--- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { ESFilter } from '../../../../../typings/elasticsearch';
import { getExceptionListItemSchemaMock } from '../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { getAnomalies, AnomaliesSearchParams } from '.';
@@ -13,8 +14,8 @@ const getFiltersFromMock = (mock: jest.Mock) => {
return searchParams.body.query.bool.filter;
};
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const getBoolCriteriaFromFilters = (filters: any[]) => filters[1].bool.must;
+const getBoolCriteriaFromFilters = (filters: ESFilter[]) =>
+ filters.find((filter) => filter?.bool?.must)?.bool?.must;
describe('getAnomalies', () => {
let searchParams: AnomaliesSearchParams;
@@ -104,4 +105,20 @@ describe('getAnomalies', () => {
])
);
});
+
+ it('ignores anomalies that do not have finalized scores', () => {
+ const mockMlAnomalySearch = jest.fn();
+ getAnomalies(searchParams, mockMlAnomalySearch);
+ const filters = getFiltersFromMock(mockMlAnomalySearch);
+
+ expect(filters).toEqual(
+ expect.arrayContaining([
+ {
+ term: {
+ is_interim: false,
+ },
+ },
+ ])
+ );
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts
index 962c44174d891..c3fea9f6d916f 100644
--- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts
+++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts
@@ -47,6 +47,7 @@ export const getAnomalies = async (
analyze_wildcard: false,
},
},
+ { term: { is_interim: false } },
{
bool: {
must: boolCriteria,
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx
index 64c085a823478..3b7baac9b80e6 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx
@@ -9,54 +9,58 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { IndexSelectPopover } from './index_select_popover';
+import { EuiComboBox } from '@elastic/eui';
-jest.mock('../../../../triggers_actions_ui/public', () => ({
- getIndexPatterns: () => {
- return ['index1', 'index2'];
- },
- firstFieldOption: () => {
- return { text: 'Select a field', value: '' };
- },
- getTimeFieldOptions: () => {
- return [
- {
- text: '@timestamp',
- value: '@timestamp',
- },
- ];
- },
- getFields: () => {
- return Promise.resolve([
- {
- name: '@timestamp',
- type: 'date',
- },
- {
- name: 'field',
- type: 'text',
- },
- ]);
- },
- getIndexOptions: () => {
- return Promise.resolve([
- {
- label: 'indexOption',
- options: [
- {
- label: 'index1',
- value: 'index1',
- },
- {
- label: 'index2',
- value: 'index2',
- },
- ],
- },
- ]);
- },
-}));
+jest.mock('../../../../triggers_actions_ui/public', () => {
+ const original = jest.requireActual('../../../../triggers_actions_ui/public');
+ return {
+ ...original,
+ getIndexPatterns: () => {
+ return ['index1', 'index2'];
+ },
+ getTimeFieldOptions: () => {
+ return [
+ {
+ text: '@timestamp',
+ value: '@timestamp',
+ },
+ ];
+ },
+ getFields: () => {
+ return Promise.resolve([
+ {
+ name: '@timestamp',
+ type: 'date',
+ },
+ {
+ name: 'field',
+ type: 'text',
+ },
+ ]);
+ },
+ getIndexOptions: () => {
+ return Promise.resolve([
+ {
+ label: 'indexOption',
+ options: [
+ {
+ label: 'index1',
+ value: 'index1',
+ },
+ {
+ label: 'index2',
+ value: 'index2',
+ },
+ ],
+ },
+ ]);
+ },
+ };
+});
describe('IndexSelectPopover', () => {
+ const onIndexChange = jest.fn();
+ const onTimeFieldChange = jest.fn();
const props = {
index: [],
esFields: [],
@@ -65,8 +69,8 @@ describe('IndexSelectPopover', () => {
index: [],
timeField: [],
},
- onIndexChange: jest.fn(),
- onTimeFieldChange: jest.fn(),
+ onIndexChange,
+ onTimeFieldChange,
};
beforeEach(() => {
@@ -106,10 +110,62 @@ describe('IndexSelectPopover', () => {
const indexComboBox = wrapper.find('#indexSelectSearchBox');
indexComboBox.first().simulate('click');
- const event = { target: { value: 'indexPattern1' } };
- indexComboBox.find('input').first().simulate('change', event);
+
+ await act(async () => {
+ const event = { target: { value: 'indexPattern1' } };
+ indexComboBox.find('input').first().simulate('change', event);
+ await nextTick();
+ wrapper.update();
+ });
const updatedIndexSearchValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]');
expect(updatedIndexSearchValue.first().props().value).toEqual('indexPattern1');
+
+ const thresholdComboBox = wrapper
+ .find(EuiComboBox)
+ .filter('[data-test-subj="thresholdIndexesComboBox"]');
+ const thresholdOptions = thresholdComboBox.prop('options');
+ expect(thresholdOptions.length > 0).toBeTruthy();
+
+ await act(async () => {
+ thresholdComboBox.prop('onChange')!([thresholdOptions[0].options![0]]);
+ await nextTick();
+ wrapper.update();
+ });
+ expect(onIndexChange).toHaveBeenCalledWith(
+ [thresholdOptions[0].options![0]].map((opt) => opt.value)
+ );
+
+ const timeFieldSelect = wrapper.find('select[data-test-subj="thresholdAlertTimeFieldSelect"]');
+ await act(async () => {
+ timeFieldSelect.simulate('change', { target: { value: '@timestamp' } });
+ await nextTick();
+ wrapper.update();
+ });
+ expect(onTimeFieldChange).toHaveBeenCalledWith('@timestamp');
+ });
+
+ test('renders index and timeField if defined', async () => {
+ const index = 'test-index';
+ const timeField = '@timestamp';
+ const indexSelectProps = {
+ ...props,
+ index: [index],
+ timeField,
+ };
+ const wrapper = mountWithIntl();
+ expect(wrapper.find('button[data-test-subj="selectIndexExpression"]').text()).toEqual(
+ `index ${index}`
+ );
+
+ wrapper.find('[data-test-subj="selectIndexExpression"]').first().simulate('click');
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
+ expect(
+ wrapper.find('EuiSelect[data-test-subj="thresholdAlertTimeFieldSelect"]').text()
+ ).toEqual(`Select a field${timeField}`);
});
});
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx
index 3349de086d982..27ddb28eed779 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx
@@ -58,9 +58,6 @@ jest.mock('../../../../triggers_actions_ui/public', () => {
getIndexPatterns: () => {
return ['index1', 'index2'];
},
- firstFieldOption: () => {
- return { text: 'Select a field', value: '' };
- },
getTimeFieldOptions: () => {
return [
{
@@ -129,6 +126,7 @@ describe('EsQueryAlertTypeExpression', () => {
index: ['test-index'],
timeField: '@timestamp',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
thresholdComparator: '>',
threshold: [0],
timeWindowSize: 15,
@@ -140,6 +138,7 @@ describe('EsQueryAlertTypeExpression', () => {
const errors = {
index: [],
esQuery: [],
+ size: [],
timeField: [],
timeWindowSize: [],
};
@@ -172,6 +171,7 @@ describe('EsQueryAlertTypeExpression', () => {
test('should render EsQueryAlertTypeExpression with expected components', async () => {
const wrapper = await setup(getAlertParams());
expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="queryJsonEditor"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy();
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx
index 27f8071564c55..37c64688ec49a 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx
@@ -30,6 +30,7 @@ import {
COMPARATORS,
ThresholdExpression,
ForLastExpression,
+ ValueExpression,
AlertTypeParamsExpressionProps,
} from '../../../../triggers_actions_ui/public';
import { validateExpression } from './validation';
@@ -45,6 +46,7 @@ const DEFAULT_VALUES = {
"match_all" : {}
}
}`,
+ SIZE: 100,
TIME_WINDOW_SIZE: 5,
TIME_WINDOW_UNIT: 'm',
THRESHOLD: [1000],
@@ -53,6 +55,7 @@ const DEFAULT_VALUES = {
const expressionFieldsWithValidation = [
'index',
'esQuery',
+ 'size',
'timeField',
'threshold0',
'threshold1',
@@ -74,6 +77,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
index,
timeField,
esQuery,
+ size,
thresholdComparator,
threshold,
timeWindowSize,
@@ -83,6 +87,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
const getDefaultParams = () => ({
...alertParams,
esQuery: esQuery ?? DEFAULT_VALUES.QUERY,
+ size: size ?? DEFAULT_VALUES.SIZE,
timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
threshold: threshold ?? DEFAULT_VALUES.THRESHOLD,
@@ -214,7 +219,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
@@ -234,6 +239,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
...alertParams,
index: indices,
esQuery: DEFAULT_VALUES.QUERY,
+ size: DEFAULT_VALUES.SIZE,
thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR,
timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT,
@@ -246,6 +252,19 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
}}
onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)}
/>
+ {
+ setParam('size', updatedValue);
+ }}
+ />
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts
index a22af7a7bc8a5..af34b88ba28c5 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts
+++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts
@@ -17,6 +17,7 @@ export interface EsQueryAlertParams extends AlertTypeParams {
index: string[];
timeField?: string;
esQuery: string;
+ size: number;
thresholdComparator?: string;
threshold: number[];
timeWindowSize: number;
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts
index 7d604e964fb9d..52278b4576557 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts
+++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts
@@ -13,6 +13,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: [],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
@@ -25,6 +26,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
@@ -37,6 +39,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
+ size: 100,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
@@ -49,6 +52,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"aggs\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
timeWindowSize: 1,
timeWindowUnit: 's',
threshold: [0],
@@ -61,6 +65,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
threshold: [],
timeWindowSize: 1,
timeWindowUnit: 's',
@@ -74,6 +79,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
threshold: [1],
timeWindowSize: 1,
timeWindowUnit: 's',
@@ -87,6 +93,7 @@ describe('expression params validation', () => {
const initialParams: EsQueryAlertParams = {
index: ['test'],
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
threshold: [10, 1],
timeWindowSize: 1,
timeWindowUnit: 's',
@@ -97,4 +104,34 @@ describe('expression params validation', () => {
'Threshold 1 must be > Threshold 0.'
);
});
+
+ test('if size property is < 0 should return proper error message', () => {
+ const initialParams: EsQueryAlertParams = {
+ index: ['test'],
+ esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
+ size: -1,
+ timeWindowSize: 1,
+ timeWindowUnit: 's',
+ threshold: [0],
+ };
+ expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0);
+ expect(validateExpression(initialParams).errors.size[0]).toBe(
+ 'Size must be between 0 and 10,000.'
+ );
+ });
+
+ test('if size property is > 10000 should return proper error message', () => {
+ const initialParams: EsQueryAlertParams = {
+ index: ['test'],
+ esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
+ size: 25000,
+ timeWindowSize: 1,
+ timeWindowUnit: 's',
+ threshold: [0],
+ };
+ expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0);
+ expect(validateExpression(initialParams).errors.size[0]).toBe(
+ 'Size must be between 0 and 10,000.'
+ );
+ });
});
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts
index 8b402d63ae565..e6449dd4a6089 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts
+++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts
@@ -10,12 +10,21 @@ import { EsQueryAlertParams } from './types';
import { ValidationResult, builtInComparators } from '../../../../triggers_actions_ui/public';
export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => {
- const { index, timeField, esQuery, threshold, timeWindowSize, thresholdComparator } = alertParams;
+ const {
+ index,
+ timeField,
+ esQuery,
+ size,
+ threshold,
+ timeWindowSize,
+ thresholdComparator,
+ } = alertParams;
const validationResult = { errors: {} };
const errors = {
index: new Array(),
timeField: new Array(),
esQuery: new Array(),
+ size: new Array(),
threshold0: new Array(),
threshold1: new Array(),
thresholdComparator: new Array(),
@@ -94,5 +103,20 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
})
);
}
+ if (!size) {
+ errors.size.push(
+ i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSizeText', {
+ defaultMessage: 'Size is required.',
+ })
+ );
+ }
+ if ((size && size < 0) || size > 10000) {
+ errors.size.push(
+ i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.invalidSizeRangeText', {
+ defaultMessage: 'Size must be between 0 and {max, number}.',
+ values: { max: 10000 },
+ })
+ );
+ }
return validationResult;
};
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx
new file mode 100644
index 0000000000000..01c2bc18f35e8
--- /dev/null
+++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx
@@ -0,0 +1,218 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mountWithIntl, nextTick } from '@kbn/test/jest';
+import { act } from 'react-dom/test-utils';
+import IndexThresholdAlertTypeExpression, { DEFAULT_VALUES } from './expression';
+import { dataPluginMock } from 'src/plugins/data/public/mocks';
+import { chartPluginMock } from 'src/plugins/charts/public/mocks';
+import { IndexThresholdAlertParams } from './types';
+import { validateExpression } from './validation';
+import {
+ builtInAggregationTypes,
+ builtInComparators,
+ getTimeUnitLabel,
+ TIME_UNITS,
+} from '../../../../triggers_actions_ui/public';
+
+jest.mock('../../../../triggers_actions_ui/public', () => {
+ const original = jest.requireActual('../../../../triggers_actions_ui/public');
+ return {
+ ...original,
+ getIndexPatterns: () => {
+ return ['index1', 'index2'];
+ },
+ getTimeFieldOptions: () => {
+ return [
+ {
+ text: '@timestamp',
+ value: '@timestamp',
+ },
+ ];
+ },
+ getFields: () => {
+ return Promise.resolve([
+ {
+ name: '@timestamp',
+ type: 'date',
+ },
+ {
+ name: 'field',
+ type: 'text',
+ },
+ ]);
+ },
+ getIndexOptions: () => {
+ return Promise.resolve([
+ {
+ label: 'indexOption',
+ options: [
+ {
+ label: 'index1',
+ value: 'index1',
+ },
+ {
+ label: 'index2',
+ value: 'index2',
+ },
+ ],
+ },
+ ]);
+ },
+ };
+});
+
+const dataMock = dataPluginMock.createStartContract();
+const chartsStartMock = chartPluginMock.createStartContract();
+
+describe('IndexThresholdAlertTypeExpression', () => {
+ function getAlertParams(overrides = {}) {
+ return {
+ index: 'test-index',
+ aggType: 'count',
+ thresholdComparator: '>',
+ threshold: [0],
+ timeWindowSize: 15,
+ timeWindowUnit: 's',
+ ...overrides,
+ };
+ }
+ async function setup(alertParams: IndexThresholdAlertParams) {
+ const { errors } = validateExpression(alertParams);
+
+ const wrapper = mountWithIntl(
+ {}}
+ setAlertProperty={() => {}}
+ errors={errors}
+ data={dataMock}
+ defaultActionGroupId=""
+ actionGroups={[]}
+ charts={chartsStartMock}
+ />
+ );
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+ return wrapper;
+ }
+
+ test(`should render IndexThresholdAlertTypeExpression with expected components when aggType doesn't require field`, async () => {
+ const wrapper = await setup(getAlertParams());
+ expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="whenExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="groupByExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeFalsy();
+ expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeFalsy();
+ });
+
+ test(`should render IndexThresholdAlertTypeExpression with expected components when aggType does require field`, async () => {
+ const wrapper = await setup(getAlertParams({ aggType: 'avg' }));
+ expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="whenExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="groupByExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeFalsy();
+ });
+
+ test(`should render IndexThresholdAlertTypeExpression with visualization when there are no expression errors`, async () => {
+ const wrapper = await setup(getAlertParams({ timeField: '@timestamp' }));
+ expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeFalsy();
+ expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeTruthy();
+ });
+
+ test(`should set default alert params when params are undefined`, async () => {
+ const wrapper = await setup(
+ getAlertParams({
+ aggType: undefined,
+ thresholdComparator: undefined,
+ timeWindowSize: undefined,
+ timeWindowUnit: undefined,
+ groupBy: undefined,
+ threshold: undefined,
+ })
+ );
+
+ expect(wrapper.find('button[data-test-subj="selectIndexExpression"]').text()).toEqual(
+ 'index test-index'
+ );
+ expect(wrapper.find('button[data-test-subj="whenExpression"]').text()).toEqual(
+ `when ${builtInAggregationTypes[DEFAULT_VALUES.AGGREGATION_TYPE].text}`
+ );
+ expect(wrapper.find('button[data-test-subj="groupByExpression"]').text()).toEqual(
+ `over ${DEFAULT_VALUES.GROUP_BY} documents `
+ );
+ expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeFalsy();
+ expect(wrapper.find('button[data-test-subj="thresholdPopover"]').text()).toEqual(
+ `${builtInComparators[DEFAULT_VALUES.THRESHOLD_COMPARATOR].text} `
+ );
+ expect(wrapper.find('button[data-test-subj="forLastExpression"]').text()).toEqual(
+ `for the last ${DEFAULT_VALUES.TIME_WINDOW_SIZE} ${getTimeUnitLabel(
+ DEFAULT_VALUES.TIME_WINDOW_UNIT as TIME_UNITS,
+ DEFAULT_VALUES.TIME_WINDOW_SIZE.toString()
+ )}`
+ );
+ expect(
+ wrapper.find('EuiEmptyPrompt[data-test-subj="visualizationPlaceholder"]').text()
+ ).toEqual(`Complete the expression to generate a preview.`);
+ });
+
+ test(`should use alert params when params are defined`, async () => {
+ const aggType = 'avg';
+ const thresholdComparator = 'between';
+ const timeWindowSize = 987;
+ const timeWindowUnit = 's';
+ const threshold = [3, 1003];
+ const groupBy = 'top';
+ const termSize = '27';
+ const termField = 'host.name';
+ const wrapper = await setup(
+ getAlertParams({
+ aggType,
+ thresholdComparator,
+ timeWindowSize,
+ timeWindowUnit,
+ termSize,
+ termField,
+ groupBy,
+ threshold,
+ })
+ );
+
+ expect(wrapper.find('button[data-test-subj="whenExpression"]').text()).toEqual(
+ `when ${builtInAggregationTypes[aggType].text}`
+ );
+ expect(wrapper.find('button[data-test-subj="groupByExpression"]').text()).toEqual(
+ `grouped over ${groupBy} ${termSize} '${termField}'`
+ );
+
+ expect(wrapper.find('button[data-test-subj="thresholdPopover"]').text()).toEqual(
+ `${builtInComparators[thresholdComparator].text} ${threshold[0]} AND ${threshold[1]}`
+ );
+ expect(wrapper.find('button[data-test-subj="forLastExpression"]').text()).toEqual(
+ `for the last ${timeWindowSize} ${getTimeUnitLabel(
+ timeWindowUnit as TIME_UNITS,
+ timeWindowSize.toString()
+ )}`
+ );
+ expect(
+ wrapper.find('EuiEmptyPrompt[data-test-subj="visualizationPlaceholder"]').text()
+ ).toEqual(`Complete the expression to generate a preview.`);
+ });
+});
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx
index aed115a53fa26..380e2793043f8 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx
@@ -28,7 +28,7 @@ import { IndexThresholdAlertParams } from './types';
import './expression.scss';
import { IndexSelectPopover } from '../components/index_select_popover';
-const DEFAULT_VALUES = {
+export const DEFAULT_VALUES = {
AGGREGATION_TYPE: 'count',
TERM_SIZE: 5,
THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN,
@@ -100,7 +100,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
alertParams[errorKey as keyof IndexThresholdAlertParams] !== undefined
);
- const canShowVizualization = !!Object.keys(errors).find(
+ const cannotShowVisualization = !!Object.keys(errors).find(
(errorKey) => expressionFieldsWithValidation.includes(errorKey) && errors[errorKey].length >= 1
);
@@ -158,6 +158,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
setAlertParams('aggType', selectedAggType)
@@ -196,6 +198,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
{aggType && builtInAggregationTypes[aggType].fieldRequired ? (
@@ -258,9 +264,10 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
/>
- {canShowVizualization ? (
+ {cannotShowVisualization ? (
@@ -275,6 +282,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
) : (
({
+ getThresholdAlertVisualizationData: jest.fn(() =>
+ Promise.resolve({
+ results: [
+ { group: 'a', metrics: [['b', 2]] },
+ { group: 'a', metrics: [['b', 10]] },
+ ],
+ })
+ ),
+}));
+
+const { getThresholdAlertVisualizationData } = jest.requireMock('./index_threshold_api');
+
+const dataMock = dataPluginMock.createStartContract();
+const chartsStartMock = chartPluginMock.createStartContract();
+dataMock.fieldFormats = ({
+ getDefaultInstance: jest.fn(() => ({
+ convert: jest.fn((s: unknown) => JSON.stringify(s)),
+ })),
+} as unknown) as DataPublicPluginStart['fieldFormats'];
+
+describe('ThresholdVisualization', () => {
+ beforeAll(() => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ uiSettings: uiSettingsServiceMock.createSetupContract(),
+ },
+ });
+ });
+
+ const alertParams = {
+ index: 'test-index',
+ aggType: 'count',
+ thresholdComparator: '>',
+ threshold: [0],
+ timeWindowSize: 15,
+ timeWindowUnit: 's',
+ };
+
+ async function setup() {
+ const wrapper = mountWithIntl(
+
+ );
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+ return wrapper;
+ }
+
+ test('periodically requests visualization data', async () => {
+ const refreshRate = 10;
+ jest.useFakeTimers();
+
+ const wrapper = mountWithIntl(
+
+ );
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+ expect(getThresholdAlertVisualizationData).toHaveBeenCalledTimes(1);
+
+ for (let i = 1; i <= 5; i++) {
+ await act(async () => {
+ jest.advanceTimersByTime(refreshRate);
+ await nextTick();
+ wrapper.update();
+ });
+ expect(getThresholdAlertVisualizationData).toHaveBeenCalledTimes(i + 1);
+ }
+ });
+
+ test('renders loading message on initial load', async () => {
+ const wrapper = mountWithIntl(
+
+ );
+ expect(wrapper.find('[data-test-subj="firstLoad"]').exists()).toBeTruthy();
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
+ expect(wrapper.find('[data-test-subj="firstLoad"]').exists()).toBeFalsy();
+ expect(getThresholdAlertVisualizationData).toHaveBeenCalled();
+ });
+
+ test('renders chart when visualization results are available', async () => {
+ const wrapper = await setup();
+
+ expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeFalsy();
+ expect(wrapper.find(Chart)).toHaveLength(1);
+ expect(wrapper.find(LineSeries)).toHaveLength(1);
+ expect(wrapper.find(LineAnnotation)).toHaveLength(1);
+ });
+
+ test('renders multiple line series chart when visualization results contain multiple groups', async () => {
+ getThresholdAlertVisualizationData.mockImplementation(() =>
+ Promise.resolve({
+ results: [
+ { group: 'a', metrics: [['b', 2]] },
+ { group: 'a', metrics: [['b', 10]] },
+ { group: 'c', metrics: [['d', 1]] },
+ ],
+ })
+ );
+
+ const wrapper = await setup();
+
+ expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeFalsy();
+ expect(wrapper.find(Chart)).toHaveLength(1);
+ expect(wrapper.find(LineSeries)).toHaveLength(2);
+ expect(wrapper.find(LineAnnotation)).toHaveLength(1);
+ });
+
+ test('renders error message when getting visualization fails', async () => {
+ const errorMessage = 'oh no';
+ getThresholdAlertVisualizationData.mockImplementation(() => Promise.reject(errorMessage));
+ const wrapper = await setup();
+
+ expect(wrapper.find('[data-test-subj="errorCallout"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="errorCallout"]').first().text()).toBe(
+ `Cannot load alert visualization${errorMessage}`
+ );
+ });
+
+ test('renders no data message when visualization results are empty', async () => {
+ getThresholdAlertVisualizationData.mockImplementation(() => Promise.resolve({ results: [] }));
+ const wrapper = await setup();
+
+ expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="noDataCallout"]').first().text()).toBe(
+ `No data matches this queryCheck that your time range and filters are correct.`
+ );
+ });
+});
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx
index 7401d0e26be68..40736f7350b1b 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx
@@ -202,6 +202,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({
if (loadingState === LoadingStateType.FirstLoad) {
return (
}
body={
@@ -220,6 +221,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({
= ({
) : (
{
index: ['[index]'],
timeField: '[timeField]',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: '>',
@@ -41,6 +42,7 @@ describe('ActionContext', () => {
index: ['[index]'],
timeField: '[timeField]',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: 'between',
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts
index 2049f9f1153dd..c38dad5134373 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts
@@ -57,6 +57,10 @@ describe('alertType', () => {
"description": "The string representation of the ES query.",
"name": "esQuery",
},
+ Object {
+ "description": "The number of hits to retrieve for each query.",
+ "name": "size",
+ },
Object {
"description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.",
"name": "threshold",
@@ -75,6 +79,7 @@ describe('alertType', () => {
index: ['index-name'],
timeField: 'time-field',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: '<',
@@ -92,6 +97,7 @@ describe('alertType', () => {
index: ['index-name'],
timeField: 'time-field',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: 'between',
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts
index 51c1fc4073d60..8fe988d95d72f 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts
@@ -23,8 +23,6 @@ import { ESSearchHit } from '../../../../../typings/elasticsearch';
export const ES_QUERY_ID = '.es-query';
-const DEFAULT_MAX_HITS_PER_EXECUTION = 1000;
-
const ActionGroupId = 'query matched';
const ConditionMetAlertInstanceId = 'query matched';
@@ -88,6 +86,13 @@ export function getAlertType(
}
);
+ const actionVariableContextSizeLabel = i18n.translate(
+ 'xpack.stackAlerts.esQuery.actionVariableContextSizeLabel',
+ {
+ defaultMessage: 'The number of hits to retrieve for each query.',
+ }
+ );
+
const actionVariableContextThresholdLabel = i18n.translate(
'xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel',
{
@@ -130,6 +135,7 @@ export function getAlertType(
params: [
{ name: 'index', description: actionVariableContextIndexLabel },
{ name: 'esQuery', description: actionVariableContextQueryLabel },
+ { name: 'size', description: actionVariableContextSizeLabel },
{ name: 'threshold', description: actionVariableContextThresholdLabel },
{ name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel },
],
@@ -160,7 +166,7 @@ export function getAlertType(
}
// During each alert execution, we run the configured query, get a hit count
- // (hits.total) and retrieve up to DEFAULT_MAX_HITS_PER_EXECUTION hits. We
+ // (hits.total) and retrieve up to params.size hits. We
// evaluate the threshold condition using the value of hits.total. If the threshold
// condition is met, the hits are counted toward the query match and we update
// the alert state with the timestamp of the latest hit. In the next execution
@@ -200,7 +206,7 @@ export function getAlertType(
from: dateStart,
to: dateEnd,
filter,
- size: DEFAULT_MAX_HITS_PER_EXECUTION,
+ size: params.size,
sortOrder: 'desc',
searchAfterSortId: undefined,
timeField: params.timeField,
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts
index a1a697446ff65..ab3ca6a2d4c31 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts
@@ -7,12 +7,17 @@
import { TypeOf } from '@kbn/config-schema';
import type { Writable } from '@kbn/utility-types';
-import { EsQueryAlertParamsSchema, EsQueryAlertParams } from './alert_type_params';
+import {
+ EsQueryAlertParamsSchema,
+ EsQueryAlertParams,
+ ES_QUERY_MAX_HITS_PER_EXECUTION,
+} from './alert_type_params';
const DefaultParams: Writable> = {
index: ['index-name'],
timeField: 'time-field',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
timeWindowSize: 5,
timeWindowUnit: 'm',
thresholdComparator: '>',
@@ -99,6 +104,28 @@ describe('alertType Params validate()', () => {
);
});
+ it('fails for invalid size', async () => {
+ delete params.size;
+ expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
+ `"[size]: expected value of type [number] but got [undefined]"`
+ );
+
+ params.size = 'foo';
+ expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
+ `"[size]: expected value of type [number] but got [string]"`
+ );
+
+ params.size = -1;
+ expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
+ `"[size]: Value must be equal to or greater than [0]."`
+ );
+
+ params.size = ES_QUERY_MAX_HITS_PER_EXECUTION + 1;
+ expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
+ `"[size]: Value must be equal to or lower than [10000]."`
+ );
+ });
+
it('fails for invalid timeWindowSize', async () => {
delete params.timeWindowSize;
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts
index 24fed92776b53..23f314b521511 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts
@@ -11,6 +11,8 @@ import { ComparatorFnNames } from '../lib';
import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server';
import { AlertTypeState } from '../../../../alerts/server';
+export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000;
+
// alert type parameters
export type EsQueryAlertParams = TypeOf;
export interface EsQueryAlertState extends AlertTypeState {
@@ -21,6 +23,7 @@ export const EsQueryAlertParamsSchemaProperties = {
index: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
timeField: schema.string({ minLength: 1 }),
esQuery: schema.string({ minLength: 1 }),
+ size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }),
timeWindowSize: schema.number({ min: 1 }),
timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }),
threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }),
diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
index 9e6a0c06808bc..d4b07203e8109 100644
--- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
+++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
@@ -2219,13 +2219,6 @@
}
}
},
- "fileUploadTelemetry": {
- "properties": {
- "filesUploadedTotalCount": {
- "type": "long"
- }
- }
- },
"maps": {
"properties": {
"settings": {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index b8a67d9c3388e..018d2d572eea0 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9676,7 +9676,6 @@
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)",
"xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件",
"xpack.infra.alerting.alertsButton": "アラート",
- "xpack.infra.alerting.createAlertButton": "アラートの作成",
"xpack.infra.alerting.logs.alertsButton": "アラート",
"xpack.infra.alerting.logs.createAlertButton": "アラートの作成",
"xpack.infra.alerting.logs.manageAlerts": "アラートを管理",
@@ -9893,7 +9892,6 @@
"xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle": "{actualCount, plural, other {件のメッセージ}}",
"xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateDescription": "通常",
"xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateTitle": "{typicalCount, plural, other {件のメッセージ}}",
- "xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "15 分ごとのログエントリー (平均)",
"xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "異常を読み込み中",
"xpack.infra.logs.analysis.anomaliesSectionTitle": "異常",
"xpack.infra.logs.analysis.anomaliesTableAnomalyDatasetName": "データセット",
@@ -9905,7 +9903,6 @@
"xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage": "この{type, select, logRate {データセット} logCategory {カテゴリ}}のログメッセージ数が想定よりも多くなっています",
"xpack.infra.logs.analysis.anomaliesTableNextPageLabel": "次のページ",
"xpack.infra.logs.analysis.anomaliesTablePreviousPageLabel": "前のページ",
- "xpack.infra.logs.analysis.anomalySectionLogRateChartNoData": "表示するログレートデータがありません。",
"xpack.infra.logs.analysis.anomalySectionNoDataBody": "時間範囲を調整する必要があるかもしれません。",
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。",
"xpack.infra.logs.analysis.createJobButtonLabel": "MLジョブを作成",
@@ -9934,8 +9931,6 @@
"xpack.infra.logs.analysis.mlUnavailableTitle": "この機能には機械学習が必要です",
"xpack.infra.logs.analysis.onboardingSuccessContent": "機械学習ロボットがデータの収集を開始するまでしばらくお待ちください。",
"xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!",
- "xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最高異常スコア",
- "xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最大異常スコア:{maxAnomalyScore}",
"xpack.infra.logs.analysis.recreateJobButtonLabel": "ML ジョブを再作成",
"xpack.infra.logs.analysis.setupFlyoutGotoListButtonLabel": "すべての機械学習ジョブ",
"xpack.infra.logs.analysis.setupFlyoutTitle": "機械学習を使用した異常検知",
@@ -9974,16 +9969,6 @@
"xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動",
"xpack.infra.logs.lastUpdate": "前回の更新 {timestamp}",
"xpack.infra.logs.loadingNewEntriesText": "新しいエントリーを読み込み中",
- "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "ドキュメンテーションを表示",
- "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "詳細について",
- "xpack.infra.logs.logAnalysis.splash.loadingMessage": "ライセンスを確認しています...",
- "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "プレースホルダー画像",
- "xpack.infra.logs.logAnalysis.splash.startTrialCta": "トライアルを開始",
- "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。",
- "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "異常検知を利用するには、無料の試用版を開始してください",
- "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "サブスクリプションのアップグレード",
- "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。",
- "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "異常検知を利用するには、プラチナサブスクリプションにアップグレードしてください",
"xpack.infra.logs.logEntryActionsDetailsButton": "詳細を表示",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "ML で分析",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "ML アプリでこのカテゴリーを分析します。",
@@ -10179,10 +10164,6 @@
"xpack.infra.metrics.alertFlyout.alertPreviewError": "このアラート条件をプレビューするときにエラーが発生しました",
"xpack.infra.metrics.alertFlyout.alertPreviewErrorDesc": "しばらくたってから再試行するか、詳細を確認してください。",
"xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "一部のデータを評価するときにエラーが発生しました。",
- "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "すべてを対象にする",
- "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データなしの件数:{boldedResultsNumber}",
- "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, other {件の結果がありました}}",
- "xpack.infra.metrics.alertFlyout.alertPreviewResult": "{firedTimes} 回発生しました",
"xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications": "結果として、このアラートは、「{alertThrottle}」に関して選択した[通知間隔]設定に基づいて{notifications}を送信しました。",
"xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber": "{notifs, plural, other {#通知}}",
"xpack.infra.metrics.alertFlyout.conditions": "条件",
@@ -19247,7 +19228,6 @@
"xpack.securitySolution.open.timeline.exportSelectedButton": "選択した項目のエクスポート",
"xpack.securitySolution.open.timeline.favoriteSelectedButton": "選択中のお気に入り",
"xpack.securitySolution.open.timeline.favoritesTooltip": "お気に入り",
- "xpack.securitySolution.open.timeline.filterByTimelineTypesTitle": "{timelineType}のみ",
"xpack.securitySolution.open.timeline.lastModifiedTableHeader": "最終更新:",
"xpack.securitySolution.open.timeline.missingSavedObjectIdTooltip": "savedObjectId がありません",
"xpack.securitySolution.open.timeline.modifiedByTableHeader": "変更者:",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 229265fe62252..5a9695b8ddc3d 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9702,7 +9702,6 @@
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)",
"xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据",
"xpack.infra.alerting.alertsButton": "告警",
- "xpack.infra.alerting.createAlertButton": "创建告警",
"xpack.infra.alerting.logs.alertsButton": "告警",
"xpack.infra.alerting.logs.createAlertButton": "创建告警",
"xpack.infra.alerting.logs.manageAlerts": "管理告警",
@@ -9919,7 +9918,6 @@
"xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle": "{actualCount, plural, other {消息}}",
"xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateDescription": "典型",
"xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateTitle": "{typicalCount, plural, other {消息}}",
- "xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "每 15 分钟日志条目数(平均值)",
"xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "正在加载异常",
"xpack.infra.logs.analysis.anomaliesSectionTitle": "异常",
"xpack.infra.logs.analysis.anomaliesTableAnomalyDatasetName": "数据集",
@@ -9931,7 +9929,6 @@
"xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage": "此{type, select, logRate {数据集} logCategory {类别}}中的日志消息多于预期",
"xpack.infra.logs.analysis.anomaliesTableNextPageLabel": "下一页",
"xpack.infra.logs.analysis.anomaliesTablePreviousPageLabel": "上一页",
- "xpack.infra.logs.analysis.anomalySectionLogRateChartNoData": "没有要显示的日志速率数据。",
"xpack.infra.logs.analysis.anomalySectionNoDataBody": "您可能想调整时间范围。",
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。",
"xpack.infra.logs.analysis.createJobButtonLabel": "创建 ML 作业",
@@ -9960,8 +9957,6 @@
"xpack.infra.logs.analysis.mlUnavailableTitle": "此功能需要 Machine Learning",
"xpack.infra.logs.analysis.onboardingSuccessContent": "请注意,我们的 Machine Learning 机器人若干分钟后才会开始收集数据。",
"xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!",
- "xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最大异常分数:",
- "xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最大异常分数:{maxAnomalyScore}",
"xpack.infra.logs.analysis.recreateJobButtonLabel": "重新创建 ML 作业",
"xpack.infra.logs.analysis.setupFlyoutGotoListButtonLabel": "所有 Machine Learning 作业",
"xpack.infra.logs.analysis.setupFlyoutTitle": "通过 Machine Learning 检测异常",
@@ -10001,16 +9996,6 @@
"xpack.infra.logs.jumpToTailText": "跳到最近的条目",
"xpack.infra.logs.lastUpdate": "上次更新时间 {timestamp}",
"xpack.infra.logs.loadingNewEntriesText": "正在加载新条目",
- "xpack.infra.logs.logAnalysis.splash.learnMoreLink": "阅读文档",
- "xpack.infra.logs.logAnalysis.splash.learnMoreTitle": "希望了解详情?",
- "xpack.infra.logs.logAnalysis.splash.loadingMessage": "正在检查许可证......",
- "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "占位符图像",
- "xpack.infra.logs.logAnalysis.splash.startTrialCta": "开始试用",
- "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。",
- "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "要访问异常检测,请启动免费试用版",
- "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "升级订阅",
- "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "必须具有白金级订阅,才能使用 Machine Learning 功能。",
- "xpack.infra.logs.logAnalysis.splash.updateSubscriptionTitle": "要访问异常检测,请升级到白金级订阅",
"xpack.infra.logs.logEntryActionsDetailsButton": "查看详情",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlButtonLabel": "在 ML 中分析",
"xpack.infra.logs.logEntryCategories.analyzeCategoryInMlTooltipDescription": "在 ML 应用中分析此类别。",
@@ -10206,12 +10191,7 @@
"xpack.infra.metrics.alertFlyout.alertPreviewError": "尝试预览此告警条件时发生错误",
"xpack.infra.metrics.alertFlyout.alertPreviewErrorDesc": "请稍后重试或查看详情了解更多信息。",
"xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "尝试评估部分数据时发生错误。",
- "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups, plural,other {# 个 {groupName}}}",
- "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "在",
- "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在 {boldedResultsNumber}无数据结果。",
- "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, other {# 个结果}}",
- "xpack.infra.metrics.alertFlyout.alertPreviewResult": "有 {firedTimes}",
- "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "在过去 {lookback} 满足此告警的条件。",
+ "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "在 {numberOfGroups, plural,other {# 个 {groupName}}}",
"xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications": "因此,此告警将根据“{alertThrottle}”的选定“通知频率”设置发送{notifications}。",
"xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber": "{notifs, plural, other {# 个通知}}",
"xpack.infra.metrics.alertFlyout.conditions": "条件",
@@ -19294,7 +19274,6 @@
"xpack.securitySolution.open.timeline.exportSelectedButton": "导出所选",
"xpack.securitySolution.open.timeline.favoriteSelectedButton": "收藏所选",
"xpack.securitySolution.open.timeline.favoritesTooltip": "收藏夹",
- "xpack.securitySolution.open.timeline.filterByTimelineTypesTitle": "仅 {timelineType}",
"xpack.securitySolution.open.timeline.lastModifiedTableHeader": "最后修改时间",
"xpack.securitySolution.open.timeline.missingSavedObjectIdTooltip": "缺失 savedObjectId",
"xpack.securitySolution.open.timeline.modifiedByTableHeader": "修改者",
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx
index ccc8e6e2080a7..b0113cdd70451 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx
@@ -66,6 +66,7 @@ export const ForLastExpression = ({
defaultMessage: 'for the last',
}
)}
+ data-test-subj="forLastExpression"
value={`${timeWindowSize} ${getTimeUnitLabel(
timeWindowUnit as TIME_UNITS,
(timeWindowSize ?? '').toString()
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx
index 5eb942b560b77..37894e6f5be98 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx
@@ -96,6 +96,7 @@ export const GroupByExpression = ({
}
)
}`}
+ data-test-subj="groupByExpression"
value={`${groupByTypes[groupBy].text} ${
groupByTypes[groupBy].sizeRequired
? `${termSize} ${termField ? `'${termField}'` : ''}`
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts
index bfcbba28b4bda..f975375adcb07 100644
--- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts
@@ -10,3 +10,4 @@ export { OfExpression } from './of';
export { GroupByExpression } from './group_by_over';
export { ThresholdExpression } from './threshold';
export { ForLastExpression } from './for_the_last';
+export { ValueExpression } from './value';
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx
new file mode 100644
index 0000000000000..e9a3dce84e149
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx
@@ -0,0 +1,136 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import { ValueExpression } from './value';
+import { mountWithIntl, nextTick } from '@kbn/test/jest';
+
+describe('value expression', () => {
+ it('renders description and value', () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper.find('[data-test-subj="valueFieldTitle"]')).toMatchInlineSnapshot(`
+
+ test
+
+ `);
+ expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(`
+
+
+
+ `);
+ });
+
+ it('renders errors', () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(`
+
+
+
+ `);
+ });
+
+ it('renders closed popover initially and opens on click', async () => {
+ const wrapper = mountWithIntl(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="valueExpression"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeFalsy();
+ expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeFalsy();
+
+ wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click');
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
+ expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeTruthy();
+ });
+
+ it('emits onChangeSelectedValue action when value is updated', async () => {
+ const onChangeSelectedValue = jest.fn();
+ const wrapper = mountWithIntl(
+
+ );
+
+ wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click');
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+ wrapper
+ .find('input[data-test-subj="valueFieldNumber"]')
+ .simulate('change', { target: { value: 3000 } });
+ expect(onChangeSelectedValue).toHaveBeenCalledWith(3000);
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx
new file mode 100644
index 0000000000000..cdf57136fe4b2
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiExpression,
+ EuiPopover,
+ EuiFieldNumber,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+} from '@elastic/eui';
+import { ClosablePopoverTitle } from './components';
+import { IErrorObject } from '../../types';
+
+interface ValueExpressionProps {
+ description: string;
+ value: number;
+ onChangeSelectedValue: (updatedValue: number) => void;
+ popupPosition?:
+ | 'upCenter'
+ | 'upLeft'
+ | 'upRight'
+ | 'downCenter'
+ | 'downLeft'
+ | 'downRight'
+ | 'leftCenter'
+ | 'leftUp'
+ | 'leftDown'
+ | 'rightCenter'
+ | 'rightUp'
+ | 'rightDown';
+ display?: 'fullWidth' | 'inline';
+ errors: string | string[] | IErrorObject;
+}
+
+export const ValueExpression = ({
+ description,
+ value,
+ onChangeSelectedValue,
+ display = 'inline',
+ popupPosition,
+ errors,
+}: ValueExpressionProps) => {
+ const [valuePopoverOpen, setValuePopoverOpen] = useState(false);
+ return (
+ {
+ setValuePopoverOpen(true);
+ }}
+ />
+ }
+ isOpen={valuePopoverOpen}
+ closePopover={() => {
+ setValuePopoverOpen(false);
+ }}
+ ownFocus
+ display={display === 'fullWidth' ? 'block' : 'inlineBlock'}
+ anchorPosition={popupPosition ?? 'downLeft'}
+ repositionOnScroll
+ >
+
+ setValuePopoverOpen(false)}
+ >
+ <>{description}>
+
+
+
+ 0 && value !== undefined}
+ error={errors}
+ >
+ 0 && value !== undefined}
+ onChange={(e: any) => {
+ onChangeSelectedValue(e.target.value as number);
+ }}
+ />
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts
index 8bbbecf8108fe..e7a22a080d79a 100644
--- a/x-pack/plugins/uptime/public/apps/plugin.ts
+++ b/x-pack/plugins/uptime/public/apps/plugin.ts
@@ -87,6 +87,28 @@ export class UptimePlugin
order: 8400,
title: PLUGIN.TITLE,
category: DEFAULT_APP_CATEGORIES.observability,
+ meta: {
+ keywords: [
+ 'Synthetics',
+ 'pings',
+ 'checks',
+ 'availability',
+ 'response duration',
+ 'response time',
+ 'outside in',
+ 'reachability',
+ 'reachable',
+ 'digital',
+ 'performance',
+ 'web performance',
+ 'web perf',
+ ],
+ searchDeepLinks: [
+ { id: 'Down monitors', title: 'Down monitors', path: '/?statusFilter=down' },
+ { id: 'Certificates', title: 'TLS Certificates', path: '/certificates' },
+ { id: 'Settings', title: 'Settings', path: '/settings' },
+ ],
+ },
mount: async (params: AppMountParameters) => {
const [coreStart, corePlugins] = await core.getStartServices();
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts
index 30fd3aea2b2dc..777caacd465d8 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts
@@ -68,6 +68,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
await createAlert({
name: 'never fire',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
thresholdComparator: '<',
threshold: [0],
});
@@ -75,6 +76,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
await createAlert({
name: 'always fire',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
+ size: 100,
thresholdComparator: '>',
threshold: [-1],
});
@@ -123,6 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
await createAlert({
name: 'never fire',
esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)),
+ size: 100,
thresholdComparator: '>=',
threshold: [0],
});
@@ -132,6 +135,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
esQuery: JSON.stringify(
rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2))
),
+ size: 100,
thresholdComparator: '>=',
threshold: [0],
});
@@ -173,6 +177,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
name: string;
timeField?: string;
esQuery: string;
+ size: number;
thresholdComparator: string;
threshold: number[];
timeWindowSize?: number;
@@ -215,6 +220,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
index: [ES_TEST_INDEX_NAME],
timeField: params.timeField || 'date',
esQuery: params.esQuery,
+ size: params.size,
timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5,
timeWindowUnit: 's',
thresholdComparator: params.thresholdComparator,
diff --git a/x-pack/test/api_integration/apis/metrics_ui/index.js b/x-pack/test/api_integration/apis/metrics_ui/index.js
index 254360ce64922..34ad92e6b89a6 100644
--- a/x-pack/test/api_integration/apis/metrics_ui/index.js
+++ b/x-pack/test/api_integration/apis/metrics_ui/index.js
@@ -8,7 +8,6 @@
export default function ({ loadTestFile }) {
describe('MetricsUI Endpoints', () => {
loadTestFile(require.resolve('./metadata'));
- loadTestFile(require.resolve('./log_analysis'));
loadTestFile(require.resolve('./log_entries'));
loadTestFile(require.resolve('./log_entry_highlights'));
loadTestFile(require.resolve('./logs_without_millis'));
diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts b/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts
deleted file mode 100644
index ecfa0cc6f2438..0000000000000
--- a/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts
+++ /dev/null
@@ -1,137 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import expect from '@kbn/expect';
-
-import { pipe } from 'fp-ts/lib/pipeable';
-import { identity } from 'fp-ts/lib/function';
-import { fold } from 'fp-ts/lib/Either';
-import {
- LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH,
- getLogEntryRateRequestPayloadRT,
- getLogEntryRateSuccessReponsePayloadRT,
-} from '../../../../plugins/infra/common/http_api/log_analysis';
-import { createPlainError, throwErrors } from '../../../../plugins/infra/common/runtime_types';
-import { FtrProviderContext } from '../../ftr_provider_context';
-
-const TIME_BEFORE_START = 1569934800000;
-const TIME_AFTER_END = 1570016700000;
-const COMMON_HEADERS = {
- 'kbn-xsrf': 'some-xsrf-token',
-};
-const ML_JOB_ID = 'kibana-logs-ui-default-default-log-entry-rate';
-
-export default ({ getService }: FtrProviderContext) => {
- const esArchiver = getService('esArchiver');
- const supertest = getService('supertest');
- const retry = getService('retry');
-
- async function createDummyJob(jobId: string) {
- await supertest
- .put(`/api/ml/anomaly_detectors/${jobId}`)
- .set(COMMON_HEADERS)
- .send({
- job_id: jobId,
- groups: [],
- analysis_config: {
- bucket_span: '15m',
- detectors: [{ function: 'count' }],
- influencers: [],
- },
- data_description: { time_field: '@timestamp' },
- analysis_limits: { model_memory_limit: '11MB' },
- model_plot_config: { enabled: false, annotations_enabled: false },
- })
- .expect(200);
- }
-
- async function deleteDummyJob(jobId: string) {
- await supertest.delete(`/api/ml/anomaly_detectors/${jobId}`).set(COMMON_HEADERS).expect(200);
-
- await retry.waitForWithTimeout(`'${jobId}' to not exist`, 5 * 1000, async () => {
- if (await supertest.get(`/api/ml/anomaly_detectors/${jobId}`).expect(404)) {
- return true;
- } else {
- throw new Error(`expected anomaly detection job '${jobId}' not to exist`);
- }
- });
- }
-
- describe('log analysis apis', () => {
- before(async () => {
- // a real ML job must exist when searching for the results
- await createDummyJob(ML_JOB_ID);
- await esArchiver.load('infra/8.0.0/ml_anomalies_partitioned_log_rate');
- });
- after(async () => {
- await deleteDummyJob(ML_JOB_ID);
- await esArchiver.unload('infra/8.0.0/ml_anomalies_partitioned_log_rate');
- });
-
- describe('log rate results', () => {
- describe('with the default source', () => {
- it('should return buckets when there are matching ml result documents', async () => {
- const { body } = await supertest
- .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH)
- .set(COMMON_HEADERS)
- .send(
- getLogEntryRateRequestPayloadRT.encode({
- data: {
- sourceId: 'default',
- timeRange: {
- startTime: TIME_BEFORE_START,
- endTime: TIME_AFTER_END,
- },
- bucketDuration: 15 * 60 * 1000,
- },
- })
- )
- .expect(200);
-
- const logEntryRateBuckets = pipe(
- getLogEntryRateSuccessReponsePayloadRT.decode(body),
- fold(throwErrors(createPlainError), identity)
- );
- expect(logEntryRateBuckets.data.bucketDuration).to.be(15 * 60 * 1000);
- expect(logEntryRateBuckets.data.histogramBuckets).to.not.be.empty();
- expect(
- logEntryRateBuckets.data.histogramBuckets.some((bucket) => {
- return bucket.partitions.some((partition) => partition.anomalies.length > 0);
- })
- ).to.be(true);
- });
-
- it('should return no buckets when there are no matching ml result documents', async () => {
- const { body } = await supertest
- .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH)
- .set(COMMON_HEADERS)
- .send(
- getLogEntryRateRequestPayloadRT.encode({
- data: {
- sourceId: 'default',
- timeRange: {
- startTime: TIME_BEFORE_START - 10 * 15 * 60 * 1000,
- endTime: TIME_BEFORE_START - 1,
- },
- bucketDuration: 15 * 60 * 1000,
- },
- })
- )
- .expect(200);
-
- const logEntryRateBuckets = pipe(
- getLogEntryRateSuccessReponsePayloadRT.decode(body),
- fold(throwErrors(createPlainError), identity)
- );
-
- expect(logEntryRateBuckets.data.bucketDuration).to.be(15 * 60 * 1000);
- expect(logEntryRateBuckets.data.histogramBuckets).to.be.empty();
- });
- });
- });
- });
-};
diff --git a/x-pack/test/api_integration/apis/metrics_ui/sources.ts b/x-pack/test/api_integration/apis/metrics_ui/sources.ts
index 7fb631477cb76..a5bab8de92f38 100644
--- a/x-pack/test/api_integration/apis/metrics_ui/sources.ts
+++ b/x-pack/test/api_integration/apis/metrics_ui/sources.ts
@@ -17,11 +17,12 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
+ const SOURCE_API_URL = '/api/metrics/source/default';
const patchRequest = async (
body: InfraSavedSourceConfiguration
): Promise => {
const response = await supertest
- .patch('/api/metrics/source/default')
+ .patch(SOURCE_API_URL)
.set('kbn-xsrf', 'xxx')
.send(body)
.expect(200);
@@ -73,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(configuration?.fields.timestamp).to.be('@timestamp');
expect(configuration?.fields.container).to.be('container.id');
expect(configuration?.logColumns).to.have.length(3);
+ expect(configuration?.anomalyThreshold).to.be(50);
expect(status?.logIndicesExist).to.be(true);
expect(status?.metricIndicesExist).to.be(true);
});
@@ -173,6 +175,31 @@ export default function ({ getService }: FtrProviderContext) {
expect(fieldColumn).to.have.property('id', 'ADDED_COLUMN_ID');
expect(fieldColumn).to.have.property('field', 'ADDED_COLUMN_FIELD');
});
+ it('validates anomalyThreshold is between range 1-100', async () => {
+ // create config with bad request
+ await supertest
+ .patch(SOURCE_API_URL)
+ .set('kbn-xsrf', 'xxx')
+ .send({ name: 'NAME', anomalyThreshold: -20 })
+ .expect(400);
+ // create config with good request
+ await supertest
+ .patch(SOURCE_API_URL)
+ .set('kbn-xsrf', 'xxx')
+ .send({ name: 'NAME', anomalyThreshold: 20 })
+ .expect(200);
+
+ await supertest
+ .patch(SOURCE_API_URL)
+ .set('kbn-xsrf', 'xxx')
+ .send({ anomalyThreshold: -2 })
+ .expect(400);
+ await supertest
+ .patch(SOURCE_API_URL)
+ .set('kbn-xsrf', 'xxx')
+ .send({ anomalyThreshold: 101 })
+ .expect(400);
+ });
});
});
}
diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
index be1ac7fbb0965..8064d498774a3 100644
--- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
@@ -772,6 +772,7 @@ export default ({ getService }: FtrProviderContext) => {
const expectedRspDatafeeds = sortBy(
testData.expected.jobs.map((job) => {
return {
+ awaitingMlNodeAllocation: false,
id: `datafeed-${job.jobId}`,
success: true,
started: testData.requestBody.startDatafeed,
diff --git a/x-pack/test/api_integration/apis/security_solution/users.ts b/x-pack/test/api_integration/apis/security_solution/users.ts
index 45e06ab72adbb..b888be2bf6276 100644
--- a/x-pack/test/api_integration/apis/security_solution/users.ts
+++ b/x-pack/test/api_integration/apis/security_solution/users.ts
@@ -23,6 +23,7 @@ export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
// Failing: See https://github.com/elastic/kibana/issues/90135
+ // Failing: See https://github.com/elastic/kibana/issues/90136
describe.skip('Users', () => {
describe('With auditbeat', () => {
before(() => esArchiver.load('auditbeat/default'));
diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js
index 9a6602ef923d3..3be24b273ae4c 100644
--- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.js
+++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.js
@@ -83,7 +83,7 @@ export default function ({ getService }) {
expect(stats.stack_stats.kibana.plugins.reporting.enabled).to.be(true);
expect(stats.stack_stats.kibana.plugins.rollups.index_patterns).to.be.an('object');
expect(stats.stack_stats.kibana.plugins.spaces.available).to.be(true);
- expect(stats.stack_stats.kibana.plugins.fileUploadTelemetry.filesUploadedTotalCount).to.be.a(
+ expect(stats.stack_stats.kibana.plugins.fileUpload.file_upload.index_creation_count).to.be.a(
'number'
);
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts
index 1d3e5244a59ed..9e84ad0a547aa 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/roles_users_utils/index.ts
@@ -6,6 +6,7 @@
*/
import { assertUnreachable } from '../../../../plugins/security_solution/common/utility_types';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
t1AnalystUser,
t2AnalystUser,
@@ -26,10 +27,9 @@ import {
} from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users';
import { ROLES } from '../../../../plugins/security_solution/common/test';
-import { FtrProviderContext } from '../../common/ftr_provider_context';
export const createUserAndRole = async (
- securityService: ReturnType,
+ getService: FtrProviderContext['getService'],
role: ROLES
): Promise => {
switch (role) {
@@ -38,32 +38,47 @@ export const createUserAndRole = async (
ROLES.detections_admin,
detectionsAdminRole,
detectionsAdminUser,
- securityService
+ getService
);
case ROLES.t1_analyst:
- return postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, securityService);
+ return postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, getService);
case ROLES.t2_analyst:
- return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, securityService);
+ return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, getService);
case ROLES.hunter:
- return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, securityService);
+ return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, getService);
case ROLES.rule_author:
- return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, securityService);
+ return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, getService);
case ROLES.soc_manager:
- return postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, securityService);
+ return postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, getService);
case ROLES.platform_engineer:
return postRoleAndUser(
ROLES.platform_engineer,
platformEngineerRole,
platformEngineerUser,
- securityService
+ getService
);
case ROLES.reader:
- return postRoleAndUser(ROLES.reader, readerRole, readerUser, securityService);
+ return postRoleAndUser(ROLES.reader, readerRole, readerUser, getService);
default:
return assertUnreachable(role);
}
};
+/**
+ * Given a roleName and security service this will delete the roleName
+ * and user
+ * @param roleName The user and role to delete with the same name
+ * @param securityService The security service
+ */
+export const deleteUserAndRole = async (
+ getService: FtrProviderContext['getService'],
+ roleName: ROLES
+): Promise => {
+ const securityService = getService('security');
+ await securityService.user.delete(roleName);
+ await securityService.role.delete(roleName);
+};
+
interface UserInterface {
password: string;
roles: string[];
@@ -95,8 +110,9 @@ export const postRoleAndUser = async (
roleName: string,
role: RoleInterface,
user: UserInterface,
- securityService: ReturnType
-) => {
+ getService: FtrProviderContext['getService']
+): Promise => {
+ const securityService = getService('security');
await securityService.role.create(roleName, {
kibana: role.kibana,
elasticsearch: role.elasticsearch,
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts
index c2822b8638d76..a319c30fa20de 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts
@@ -14,16 +14,14 @@ import {
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { deleteSignalsIndex } from '../../utils';
import { ROLES } from '../../../../plugins/security_solution/common/test';
-import { createUserAndRole } from '../roles_users_utils';
+import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
- const security = getService('security');
- // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/90229
- describe.skip('create_index', () => {
+ describe('create_index', () => {
afterEach(async () => {
await deleteSignalsIndex(supertest);
});
@@ -66,8 +64,13 @@ export default ({ getService }: FtrProviderContext) => {
describe('t1_analyst', () => {
const role = ROLES.t1_analyst;
+
beforeEach(async () => {
- await createUserAndRole(security, role);
+ await createUserAndRole(getService, role);
+ });
+
+ afterEach(async () => {
+ await deleteUserAndRole(getService, role);
});
it('should return a 404 when the signal index has never been created', async () => {
@@ -88,7 +91,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(403);
expect(body).to.eql({
message:
- 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [t1_analyst], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]',
+ 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [t1_analyst], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]',
status_code: 403,
});
});
@@ -111,8 +114,13 @@ export default ({ getService }: FtrProviderContext) => {
describe('t2_analyst', () => {
const role = ROLES.t2_analyst;
+
beforeEach(async () => {
- await createUserAndRole(security, role);
+ await createUserAndRole(getService, role);
+ });
+
+ afterEach(async () => {
+ await deleteUserAndRole(getService, role);
});
it('should return a 404 when the signal index has never been created', async () => {
@@ -133,7 +141,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(403);
expect(body).to.eql({
message:
- 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [t2_analyst], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]',
+ 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [t2_analyst], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]',
status_code: 403,
});
});
@@ -156,8 +164,13 @@ export default ({ getService }: FtrProviderContext) => {
describe('detections_admin', () => {
const role = ROLES.detections_admin;
+
beforeEach(async () => {
- await createUserAndRole(security, role);
+ await createUserAndRole(getService, role);
+ });
+
+ afterEach(async () => {
+ await deleteUserAndRole(getService, role);
});
it('should return a 404 when the signal index has never been created', async () => {
@@ -201,8 +214,13 @@ export default ({ getService }: FtrProviderContext) => {
describe('soc_manager', () => {
const role = ROLES.soc_manager;
+
beforeEach(async () => {
- await createUserAndRole(security, role);
+ await createUserAndRole(getService, role);
+ });
+
+ afterEach(async () => {
+ await deleteUserAndRole(getService, role);
});
it('should return a 404 when the signal index has never been created', async () => {
@@ -223,7 +241,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(403);
expect(body).to.eql({
message:
- 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [soc_manager], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]',
+ 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [soc_manager], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]',
status_code: 403,
});
});
@@ -246,8 +264,13 @@ export default ({ getService }: FtrProviderContext) => {
describe('hunter', () => {
const role = ROLES.hunter;
+
beforeEach(async () => {
- await createUserAndRole(security, role);
+ await createUserAndRole(getService, role);
+ });
+
+ afterEach(async () => {
+ await deleteUserAndRole(getService, role);
});
it('should return a 404 when the signal index has never been created', async () => {
@@ -268,7 +291,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(403);
expect(body).to.eql({
message:
- 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [hunter], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]',
+ 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [hunter], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]',
status_code: 403,
});
});
@@ -291,8 +314,13 @@ export default ({ getService }: FtrProviderContext) => {
describe('platform_engineer', () => {
const role = ROLES.platform_engineer;
+
beforeEach(async () => {
- await createUserAndRole(security, role);
+ await createUserAndRole(getService, role);
+ });
+
+ afterEach(async () => {
+ await deleteUserAndRole(getService, role);
});
it('should return a 404 when the signal index has never been created', async () => {
@@ -336,8 +364,13 @@ export default ({ getService }: FtrProviderContext) => {
describe('reader', () => {
const role = ROLES.reader;
+
beforeEach(async () => {
- await createUserAndRole(security, role);
+ await createUserAndRole(getService, role);
+ });
+
+ afterEach(async () => {
+ await deleteUserAndRole(getService, role);
});
it('should return a 404 when the signal index has never been created', async () => {
@@ -358,7 +391,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(403);
expect(body).to.eql({
message:
- 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [reader], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]',
+ 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [reader], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]',
status_code: 403,
});
});
@@ -381,8 +414,13 @@ export default ({ getService }: FtrProviderContext) => {
describe('rule_author', () => {
const role = ROLES.rule_author;
+
beforeEach(async () => {
- await createUserAndRole(security, role);
+ await createUserAndRole(getService, role);
+ });
+
+ afterEach(async () => {
+ await deleteUserAndRole(getService, role);
});
it('should return a 404 when the signal index has never been created', async () => {
@@ -403,7 +441,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(403);
expect(body).to.eql({
message:
- 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [rule_author], this action is granted by the privileges [read_ilm,manage_ilm,manage,all]',
+ 'security_exception: action [cluster:admin/ilm/get] is unauthorized for user [rule_author], this action is granted by the cluster privileges [read_ilm,manage_ilm,manage,all]',
status_code: 403,
});
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts
index fc77fba5fa339..dd0052b03382a 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts
@@ -21,7 +21,7 @@ import {
getIndexNameFromLoad,
waitForIndexToPopulate,
} from '../../utils';
-import { createUserAndRole } from '../roles_users_utils';
+import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
interface CreateResponse {
index: string;
@@ -34,7 +34,6 @@ export default ({ getService }: FtrProviderContext): void => {
const es = getService('es');
const esArchiver = getService('esArchiver');
const kbnClient = getService('kibanaServer');
- const security = getService('security');
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
@@ -173,7 +172,7 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('rejects the request if the user does not have sufficient privileges', async () => {
- await createUserAndRole(security, ROLES.t1_analyst);
+ await createUserAndRole(getService, ROLES.t1_analyst);
await supertestWithoutAuth
.post(DETECTION_ENGINE_SIGNALS_MIGRATION_URL)
@@ -181,6 +180,8 @@ export default ({ getService }: FtrProviderContext): void => {
.auth(ROLES.t1_analyst, 'changeme')
.send({ index: [legacySignalsIndexName] })
.expect(400);
+
+ await deleteUserAndRole(getService, ROLES.t1_analyst);
});
});
};
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts
index 234370e4f104e..bba6ce1125c37 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts
@@ -32,12 +32,10 @@ interface FinalizeResponse extends CreateResponse {
export default ({ getService }: FtrProviderContext): void => {
const es = getService('es');
const esArchiver = getService('esArchiver');
- const security = getService('security');
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
- // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/90229
- describe.skip('deleting signals migrations', () => {
+ describe('deleting signals migrations', () => {
let outdatedSignalsIndexName: string;
let createdMigration: CreateResponse;
let finalizedMigration: FinalizeResponse;
@@ -105,7 +103,7 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('rejects the request if the user does not have sufficient privileges', async () => {
- await createUserAndRole(security, ROLES.t1_analyst);
+ await createUserAndRole(getService, ROLES.t1_analyst);
const { body } = await supertestWithoutAuth
.delete(DETECTION_ENGINE_SIGNALS_MIGRATION_URL)
@@ -119,7 +117,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(deletedMigration.id).to.eql(createdMigration.migration_id);
expect(deletedMigration.error).to.eql({
message:
- 'security_exception: action [indices:admin/settings/update] is unauthorized for user [t1_analyst] on indices [], this action is granted by the privileges [manage,all]',
+ 'security_exception: action [indices:admin/settings/update] is unauthorized for user [t1_analyst] on indices [], this action is granted by the index privileges [manage,all]',
status_code: 403,
});
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts
index 64c0c6666469a..0fd05904d5e33 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts
@@ -21,7 +21,7 @@ import {
getIndexNameFromLoad,
waitFor,
} from '../../utils';
-import { createUserAndRole } from '../roles_users_utils';
+import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
interface StatusResponse {
index: string;
@@ -44,12 +44,10 @@ interface FinalizeResponse {
export default ({ getService }: FtrProviderContext): void => {
const esArchiver = getService('esArchiver');
const kbnClient = getService('kibanaServer');
- const security = getService('security');
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
- // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/88302
- describe.skip('Finalizing signals migrations', () => {
+ describe('Finalizing signals migrations', () => {
let legacySignalsIndexName: string;
let outdatedSignalsIndexName: string;
let createdMigrations: CreateResponse[];
@@ -234,7 +232,7 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('rejects the request if the user does not have sufficient privileges', async () => {
- await createUserAndRole(security, ROLES.t1_analyst);
+ await createUserAndRole(getService, ROLES.t1_analyst);
const { body } = await supertestWithoutAuth
.post(DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL)
@@ -249,9 +247,11 @@ export default ({ getService }: FtrProviderContext): void => {
expect(finalizeResponse.completed).not.to.eql(true);
expect(finalizeResponse.error).to.eql({
message:
- 'security_exception: action [cluster:monitor/task/get] is unauthorized for user [t1_analyst]',
+ 'security_exception: action [cluster:monitor/task/get] is unauthorized for user [t1_analyst], this action is granted by the cluster privileges [monitor,manage,all]',
status_code: 403,
});
+
+ await deleteUserAndRole(getService, ROLES.t1_analyst);
});
});
};
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts
index 3b9ec8e0909dc..793dec9eaae4b 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts
@@ -11,12 +11,11 @@ import { DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL } from '../../../../plugi
import { ROLES } from '../../../../plugins/security_solution/common/test';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { createSignalsIndex, deleteSignalsIndex, getIndexNameFromLoad } from '../../utils';
-import { createUserAndRole } from '../roles_users_utils';
+import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
const esArchiver = getService('esArchiver');
- const security = getService('security');
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
@@ -99,7 +98,7 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('rejects the request if the user does not have sufficient privileges', async () => {
- await createUserAndRole(security, ROLES.t1_analyst);
+ await createUserAndRole(getService, ROLES.t1_analyst);
await supertestWithoutAuth
.get(DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL)
@@ -107,6 +106,8 @@ export default ({ getService }: FtrProviderContext): void => {
.auth(ROLES.t1_analyst, 'changeme')
.query({ from: '2020-10-10' })
.expect(403);
+
+ await deleteUserAndRole(getService, ROLES.t1_analyst);
});
});
};
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts
index 973b643a7a425..36a05f0ae8c0e 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts
@@ -27,7 +27,7 @@ import {
waitForRuleSuccessOrStatus,
getRuleForSignalTesting,
} from '../../utils';
-import { createUserAndRole } from '../roles_users_utils';
+import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
import { ROLES } from '../../../../plugins/security_solution/common/test';
// eslint-disable-next-line import/no-default-export
@@ -35,7 +35,6 @@ export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const supertestWithoutAuth = getService('supertestWithoutAuth');
- const securityService = getService('security');
describe('open_close_signals', () => {
describe('validation checks', () => {
@@ -172,7 +171,7 @@ export default ({ getService }: FtrProviderContext) => {
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
- await createUserAndRole(securityService, ROLES.t1_analyst);
+ await createUserAndRole(getService, ROLES.t1_analyst);
const signalsOpen = await getSignalsByIds(supertest, [id]);
const signalIds = signalsOpen.hits.hits.map((signal) => signal._id);
@@ -203,6 +202,8 @@ export default ({ getService }: FtrProviderContext) => {
}) => status === 'closed'
);
expect(everySignalOpen).to.eql(true);
+
+ await deleteUserAndRole(getService, ROLES.t1_analyst);
});
it('should be able to close signals with soc_manager user', async () => {
@@ -211,7 +212,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const userAndRole = ROLES.soc_manager;
- await createUserAndRole(securityService, userAndRole);
+ await createUserAndRole(getService, userAndRole);
const signalsOpen = await getSignalsByIds(supertest, [id]);
const signalIds = signalsOpen.hits.hits.map((signal) => signal._id);
@@ -240,6 +241,8 @@ export default ({ getService }: FtrProviderContext) => {
}) => status === 'closed'
);
expect(everySignalClosed).to.eql(true);
+
+ await deleteUserAndRole(getService, userAndRole);
});
});
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts
index 614f08295b38f..f8949daea831e 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts
@@ -10,14 +10,14 @@ import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../plugins/security_so
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { ROLES } from '../../../../plugins/security_solution/common/test';
+import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
- // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/90229
- describe.skip('read_privileges', () => {
+ describe('read_privileges', () => {
it('should return expected privileges for elastic admin', async () => {
const { body } = await supertest.get(DETECTION_ENGINE_PRIVILEGES_URL).send().expect(200);
expect(body).to.eql({
@@ -78,6 +78,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should return expected privileges for a "reader" user', async () => {
+ await createUserAndRole(getService, ROLES.reader);
const { body } = await supertestWithoutAuth
.get(DETECTION_ENGINE_PRIVILEGES_URL)
.auth(ROLES.reader, 'changeme')
@@ -138,9 +139,11 @@ export default ({ getService }: FtrProviderContext) => {
is_authenticated: true,
has_encryption_key: true,
});
+ await deleteUserAndRole(getService, ROLES.reader);
});
it('should return expected privileges for a "t1_analyst" user', async () => {
+ await createUserAndRole(getService, ROLES.t1_analyst);
const { body } = await supertestWithoutAuth
.get(DETECTION_ENGINE_PRIVILEGES_URL)
.auth(ROLES.t1_analyst, 'changeme')
@@ -201,9 +204,11 @@ export default ({ getService }: FtrProviderContext) => {
is_authenticated: true,
has_encryption_key: true,
});
+ await deleteUserAndRole(getService, ROLES.t1_analyst);
});
it('should return expected privileges for a "t2_analyst" user', async () => {
+ await createUserAndRole(getService, ROLES.t2_analyst);
const { body } = await supertestWithoutAuth
.get(DETECTION_ENGINE_PRIVILEGES_URL)
.auth(ROLES.t2_analyst, 'changeme')
@@ -264,9 +269,11 @@ export default ({ getService }: FtrProviderContext) => {
is_authenticated: true,
has_encryption_key: true,
});
+ await deleteUserAndRole(getService, ROLES.t2_analyst);
});
it('should return expected privileges for a "hunter" user', async () => {
+ await createUserAndRole(getService, ROLES.hunter);
const { body } = await supertestWithoutAuth
.get(DETECTION_ENGINE_PRIVILEGES_URL)
.auth(ROLES.hunter, 'changeme')
@@ -327,9 +334,11 @@ export default ({ getService }: FtrProviderContext) => {
is_authenticated: true,
has_encryption_key: true,
});
+ await deleteUserAndRole(getService, ROLES.hunter);
});
it('should return expected privileges for a "rule_author" user', async () => {
+ await createUserAndRole(getService, ROLES.rule_author);
const { body } = await supertestWithoutAuth
.get(DETECTION_ENGINE_PRIVILEGES_URL)
.auth(ROLES.rule_author, 'changeme')
@@ -390,9 +399,11 @@ export default ({ getService }: FtrProviderContext) => {
is_authenticated: true,
has_encryption_key: true,
});
+ await deleteUserAndRole(getService, ROLES.rule_author);
});
it('should return expected privileges for a "soc_manager" user', async () => {
+ await createUserAndRole(getService, ROLES.soc_manager);
const { body } = await supertestWithoutAuth
.get(DETECTION_ENGINE_PRIVILEGES_URL)
.auth(ROLES.soc_manager, 'changeme')
@@ -453,9 +464,11 @@ export default ({ getService }: FtrProviderContext) => {
is_authenticated: true,
has_encryption_key: true,
});
+ await deleteUserAndRole(getService, ROLES.soc_manager);
});
it('should return expected privileges for a "platform_engineer" user', async () => {
+ await createUserAndRole(getService, ROLES.platform_engineer);
const { body } = await supertestWithoutAuth
.get(DETECTION_ENGINE_PRIVILEGES_URL)
.auth(ROLES.platform_engineer, 'changeme')
@@ -516,9 +529,11 @@ export default ({ getService }: FtrProviderContext) => {
is_authenticated: true,
has_encryption_key: true,
});
+ await deleteUserAndRole(getService, ROLES.platform_engineer);
});
it('should return expected privileges for a "detections_admin" user', async () => {
+ await createUserAndRole(getService, ROLES.detections_admin);
const { body } = await supertestWithoutAuth
.get(DETECTION_ENGINE_PRIVILEGES_URL)
.auth(ROLES.detections_admin, 'changeme')
@@ -579,6 +594,7 @@ export default ({ getService }: FtrProviderContext) => {
is_authenticated: true,
has_encryption_key: true,
});
+ await deleteUserAndRole(getService, ROLES.detections_admin);
});
});
};
diff --git a/x-pack/test/fleet_api_integration/apis/agents/enroll.ts b/x-pack/test/fleet_api_integration/apis/agents/enroll.ts
index 96c472697801e..3358d045fe69b 100644
--- a/x-pack/test/fleet_api_integration/apis/agents/enroll.ts
+++ b/x-pack/test/fleet_api_integration/apis/agents/enroll.ts
@@ -18,8 +18,9 @@ export default function (providerContext: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const esClient = getService('es');
const kibanaServer = getService('kibanaServer');
-
+ const supertestWithAuth = getService('supertest');
const supertest = getSupertestWithoutAuth(providerContext);
+
let apiKey: { id: string; api_key: string };
let kibanaVersion: string;
@@ -58,6 +59,51 @@ export default function (providerContext: FtrProviderContext) {
await esArchiver.unload('fleet/agents');
});
+ it('should not allow enrolling in a managed policy', async () => {
+ // update existing policy to managed
+ await supertestWithAuth
+ .put(`/api/fleet/agent_policies/policy1`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'Test policy',
+ namespace: 'default',
+ is_managed: true,
+ })
+ .expect(200);
+
+ // try to enroll in managed policy
+ const { body } = await supertest
+ .post(`/api/fleet/agents/enroll`)
+ .set('kbn-xsrf', 'xxx')
+ .set(
+ 'Authorization',
+ `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}`
+ )
+ .send({
+ type: 'PERMANENT',
+ metadata: {
+ local: {
+ elastic: { agent: { version: kibanaVersion } },
+ },
+ user_provided: {},
+ },
+ })
+ .expect(400);
+
+ expect(body.message).to.contain('Cannot enroll in managed policy');
+
+ // restore to original (unmanaged)
+ await supertestWithAuth
+ .put(`/api/fleet/agent_policies/policy1`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'Test policy',
+ namespace: 'default',
+ is_managed: false,
+ })
+ .expect(200);
+ });
+
it('should not allow to enroll an agent with a invalid enrollment', async () => {
await supertest
.post(`/api/fleet/agents/enroll`)
diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts
index 9fbe5373dd3d4..f8f852b9667fb 100644
--- a/x-pack/test/functional/apps/index_management/home_page.ts
+++ b/x-pack/test/functional/apps/index_management/home_page.ts
@@ -13,6 +13,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
const log = getService('log');
const browser = getService('browser');
+ const retry = getService('retry');
describe('Home page', function () {
before(async () => {
@@ -49,8 +50,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(url).to.contain(`/data_streams`);
// Verify content
- const dataStreamList = await testSubjects.exists('dataStreamList');
- expect(dataStreamList).to.be(true);
+ await retry.waitFor('Wait until dataStream Table is visible.', async () => {
+ return await testSubjects.isDisplayed('dataStreamTable');
+ });
});
});
diff --git a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js
index 4496b59393eec..0ce9b7022b38d 100644
--- a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js
+++ b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js
@@ -11,7 +11,6 @@ import uuid from 'uuid/v4';
export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['maps', 'common']);
- const testSubjects = getService('testSubjects');
const log = getService('log');
const security = getService('security');
@@ -99,20 +98,6 @@ export default function ({ getService, getPageObjects }) {
expect(newIndexedLayerExists).to.be(false);
});
- it('should create a link to new index in management', async () => {
- const indexName = await indexPoint();
-
- const layerAddReady = await PageObjects.maps.importLayerReadyForAdd();
- expect(layerAddReady).to.be(true);
-
- const newIndexLinkExists = await testSubjects.exists('indexManagementNewIndexLink');
- expect(newIndexLinkExists).to.be(true);
-
- const indexLink = await testSubjects.getAttribute('indexManagementNewIndexLink', 'href');
- const linkDirectsToNewIndex = indexLink.endsWith(indexName);
- expect(linkDirectsToNewIndex).to.be(true);
- });
-
const GEO_POINT = 'geo_point';
const pointGeojsonFiles = ['point.json', 'multi_point.json'];
pointGeojsonFiles.forEach(async (pointFile) => {
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts
index 72e81dad44629..04712fc0c1426 100644
--- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts
@@ -15,8 +15,7 @@ export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
- // Failing ES promotion, see https://github.com/elastic/kibana/issues/89980
- describe.skip('jobs cloning supported by UI form', function () {
+ describe('jobs cloning supported by UI form', function () {
const testDataList: Array<{
suiteTitle: string;
archive: string;
diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts
index d2d6a24bdccd1..55a54245cf832 100644
--- a/x-pack/test/functional/apps/status_page/status_page.ts
+++ b/x-pack/test/functional/apps/status_page/status_page.ts
@@ -14,7 +14,8 @@ export default function statusPageFunctonalTests({
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['security', 'statusPage', 'home']);
- describe('Status Page', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/50448
+ describe.skip('Status Page', function () {
this.tags(['skipCloud', 'includeFirefox']);
before(async () => await esArchiver.load('empty_kibana'));
after(async () => await esArchiver.unload('empty_kibana'));
diff --git a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts
index 46e0c01afcc38..b8d6b88e4ed9a 100644
--- a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts
+++ b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts
@@ -15,7 +15,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const appsMenu = getService('appsMenu');
const managementMenu = getService('managementMenu');
- describe('security', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/90576
+ describe.skip('security', () => {
before(async () => {
await esArchiver.load('empty_kibana');
await PageObjects.security.forceLogout();
diff --git a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts
index 711c9b7683678..93955fb741044 100644
--- a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts
+++ b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts
@@ -18,7 +18,8 @@ export default function upgradeAssistantFunctionalTests({
const log = getService('log');
const retry = getService('retry');
- describe('Upgrade Checkup', function () {
+ // Failing: See https://github.com/elastic/kibana/issues/86546
+ describe.skip('Upgrade Checkup', function () {
this.tags('includeFirefox');
before(async () => {
diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts
index e3f1d04640754..15b4773373bf7 100644
--- a/x-pack/test/functional/apps/uptime/locations.ts
+++ b/x-pack/test/functional/apps/uptime/locations.ts
@@ -39,8 +39,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await makeChecksWithStatus(es, LessAvailMonitor, 5, 2, 10000, {}, 'down');
};
- // FLAKY: https://github.com/elastic/kibana/issues/85208
- describe.skip('Observer location', () => {
+ describe('Observer location', () => {
const start = '~ 15 minutes ago';
const end = 'now';
diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts
index e0abee38f4195..9c39ed7017721 100644
--- a/x-pack/test/functional/apps/uptime/ping_redirects.ts
+++ b/x-pack/test/functional/apps/uptime/ping_redirects.ts
@@ -19,8 +19,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const monitor = () => uptime.monitor;
- // FLAKY: https://github.com/elastic/kibana/issues/84992
- describe.skip('Ping redirects', () => {
+ describe('Ping redirects', () => {
const start = '~ 15 minutes ago';
const end = 'now';
diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts
index 5be8eee3155b9..a7259f2410d6b 100644
--- a/x-pack/test/functional_with_es_ssl/config.ts
+++ b/x-pack/test/functional_with_es_ssl/config.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import Fs from 'fs';
import { resolve, join } from 'path';
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
@@ -33,6 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
elasticsearch: {
...xpackFunctionalConfig.get('servers.elasticsearch'),
protocol: 'https',
+ certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)],
},
};
diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json
index 7ba5c00a71b37..4cbec2da21807 100644
--- a/x-pack/test/tsconfig.json
+++ b/x-pack/test/tsconfig.json
@@ -82,9 +82,10 @@
{ "path": "../plugins/ui_actions_enhanced/tsconfig.json" },
{ "path": "../plugins/upgrade_assistant/tsconfig.json" },
{ "path": "../plugins/watcher/tsconfig.json" },
- { "path": "../plugins/runtime_fields/tsconfig.json" },
- { "path": "../plugins/index_management/tsconfig.json" },
- { "path": "../plugins/watcher/tsconfig.json" },
+ { "path": "../plugins/rollup/tsconfig.json" },
+ { "path": "../plugins/remote_clusters/tsconfig.json" },
+ { "path": "../plugins/cross_cluster_replication/tsconfig.json" },
+ { "path": "../plugins/index_lifecycle_management/tsconfig.json"},
{ "path": "../plugins/uptime/tsconfig.json" }
]
}
diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json
index 3afbb027e7fde..00286ac47da6e 100644
--- a/x-pack/tsconfig.json
+++ b/x-pack/tsconfig.json
@@ -27,7 +27,6 @@
"plugins/licensing/**/*",
"plugins/lens/**/*",
"plugins/maps/**/*",
- "plugins/maps_file_upload/**/*",
"plugins/maps_legacy_licensing/**/*",
"plugins/ml/**/*",
"plugins/observability/**/*",
@@ -57,6 +56,10 @@
"plugins/index_management/**/*",
"plugins/grokdebugger/**/*",
"plugins/upgrade_assistant/**/*",
+ "plugins/rollup/**/*",
+ "plugins/remote_clusters/**/*",
+ "plugins/cross_cluster_replication/**/*",
+ "plugins/index_lifecycle_management/**/*",
"plugins/uptime/**/*",
"test/**/*"
],
@@ -126,7 +129,6 @@
{ "path": "./plugins/lens/tsconfig.json" },
{ "path": "./plugins/license_management/tsconfig.json" },
{ "path": "./plugins/licensing/tsconfig.json" },
- { "path": "./plugins/maps_file_upload/tsconfig.json" },
{ "path": "./plugins/maps_legacy_licensing/tsconfig.json" },
{ "path": "./plugins/maps/tsconfig.json" },
{ "path": "./plugins/ml/tsconfig.json" },
@@ -148,6 +150,10 @@
{ "path": "./plugins/runtime_fields/tsconfig.json" },
{ "path": "./plugins/index_management/tsconfig.json" },
{ "path": "./plugins/watcher/tsconfig.json" },
+ { "path": "./plugins/rollup/tsconfig.json" },
+ { "path": "./plugins/remote_clusters/tsconfig.json" },
+ { "path": "./plugins/cross_cluster_replication/tsconfig.json"},
+ { "path": "./plugins/index_lifecycle_management/tsconfig.json"},
{ "path": "./plugins/uptime/tsconfig.json" }
]
}
diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json
index 54cee9b124237..a36f4e205ab7d 100644
--- a/x-pack/tsconfig.refs.json
+++ b/x-pack/tsconfig.refs.json
@@ -29,7 +29,6 @@
{ "path": "./plugins/lens/tsconfig.json" },
{ "path": "./plugins/license_management/tsconfig.json" },
{ "path": "./plugins/licensing/tsconfig.json" },
- { "path": "./plugins/maps_file_upload/tsconfig.json" },
{ "path": "./plugins/maps_legacy_licensing/tsconfig.json" },
{ "path": "./plugins/maps/tsconfig.json" },
{ "path": "./plugins/ml/tsconfig.json" },
@@ -52,6 +51,10 @@
{ "path": "./plugins/runtime_fields/tsconfig.json" },
{ "path": "./plugins/index_management/tsconfig.json" },
{ "path": "./plugins/watcher/tsconfig.json" },
+ { "path": "./plugins/rollup/tsconfig.json"},
+ { "path": "./plugins/remote_clusters/tsconfig.json"},
+ { "path": "./plugins/cross_cluster_replication/tsconfig.json"},
+ { "path": "./plugins/index_lifecycle_management/tsconfig.json"},
{ "path": "./plugins/uptime/tsconfig.json" }
]
}
diff --git a/yarn.lock b/yarn.lock
index ec6cf338a43da..c9f3186ffcba2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2187,10 +2187,10 @@
pump "^3.0.0"
secure-json-parse "^2.1.0"
-"@elastic/ems-client@7.11.0":
- version "7.11.0"
- resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.11.0.tgz#d2142d0ef5bd1aff7ae67b37c1394b73cdd48d8b"
- integrity sha512-7+gDEkBr8nRS7X9i/UPg1WkS7bEBuNbBBjXCchQeYwqPRmw6vOb4wjlNzVwmOFsp2OH4lVFfZ+XU4pxTt32EXA==
+"@elastic/ems-client@7.12.0":
+ version "7.12.0"
+ resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.12.0.tgz#cf83f5ad76e26cedfa6f5b91277d2d919b9423d1"
+ integrity sha512-Svv3boWL1n14nIt6tL9gaA9Ym1B4AwWl6ISZT62+uKM2G+imZxWLkqpQc/HHcf7TfuAmleF2NFwnT5vw2vZTpA==
dependencies:
lodash "^4.17.15"
semver "7.3.2"