+
{onClick ? (
) : (
-
- {fieldIcon &&
{fieldIcon}}
- {fieldName &&
{fieldName}}
- {fieldInfoIcon &&
{fieldInfoIcon}
}
+
+ {innerContent}
)}
diff --git a/x-pack/plugins/lens/public/_mixins.scss b/x-pack/plugins/lens/public/_mixins.scss
index a3cf6caa5a429b..0db72d118cef14 100644
--- a/x-pack/plugins/lens/public/_mixins.scss
+++ b/x-pack/plugins/lens/public/_mixins.scss
@@ -11,3 +11,39 @@
transparentize(red, .9) 100%
);
}
+
+// Static styles for a draggable item
+@mixin lnsDraggable {
+ @include euiSlightShadow;
+ background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade);
+ border: $euiBorderWidthThin dashed transparent;
+ cursor: grab;
+}
+
+// Static styles for a drop area
+@mixin lnsDroppable {
+ border: $euiBorderWidthThin dashed $euiBorderColor;
+}
+
+// Hovering state for drag item and drop area
+@mixin lnsDragDropHover {
+ &:hover {
+ border: $euiBorderWidthThin dashed $euiColorMediumShade;
+ }
+}
+
+// Style for drop area when there's an item being dragged
+@mixin lnsDroppableActive {
+ background-color: transparentize($euiColorVis0, .9);
+}
+
+// Style for drop area while hovering with item
+@mixin lnsDroppableActiveHover {
+ background-color: transparentize($euiColorVis0, .75);
+ border: $euiBorderWidthThin dashed $euiColorVis0;
+}
+
+// Style for drop area that is not allowed for current item
+@mixin lnsDroppableNotAllowed {
+ opacity: .5;
+}
diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
index 3581151dd5f768..dc53f3a2bc2a77 100644
--- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
+++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap
@@ -1,17 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DragDrop droppable is reflected in the className 1`] = `
-
Hello!
-
+
`;
exports[`DragDrop items that have droppable=false get special styling when another item is dragged 1`] = `
-
Hello!
-
+
`;
exports[`DragDrop renders if nothing is being dragged 1`] = `
-
Hello!
-
+
`;
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
index c971540e165c11..410aaef9a51951 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss
@@ -1,13 +1,54 @@
-.lnsDragDrop-isNotDroppable {
- opacity: .5;
+@import '../variables';
+@import '../mixins';
+
+.lnsDragDrop {
+ transition: background-color $euiAnimSpeedFast ease-in-out, border-color $euiAnimSpeedFast ease-in-out;
+}
+
+// Draggable item
+.lnsDragDrop-isDraggable {
+ @include lnsDraggable;
+ @include lnsDragDropHover;
+
+ // Include a possible nested button like when using FieldButton
+ > .kbnFieldButton__button {
+ cursor: grab;
+ }
+
+ &:focus {
+ @include euiFocusRing;
+ }
+}
+
+// Draggable item when it is moving
+.lnsDragDrop-isHidden {
+ opacity: 0;
+}
+
+// Drop area
+.lnsDragDrop-isDroppable {
+ @include lnsDroppable;
}
-// Fix specificity by chaining classes
+// Drop area when there's an item being dragged
+.lnsDragDrop-isDropTarget {
+ @include lnsDroppableActive;
+}
-.lnsDragDrop.lnsDragDrop-isDropTarget {
- background-color: transparentize($euiColorSecondary, .9);
+// Drop area while hovering with item
+.lnsDragDrop-isActiveDropTarget {
+ @include lnsDroppableActiveHover;
+}
+
+// Drop area that is not allowed for current item
+.lnsDragDrop-isNotDroppable {
+ @include lnsDroppableNotAllowed;
}
-.lnsDragDrop.lnsDragDrop-isActiveDropTarget {
- background-color: transparentize($euiColorSecondary, .75);
+// Drop area will be replacing existing content
+.lnsDragDrop-isReplacing {
+ &,
+ .lnsLayerPanel__triggerLink {
+ text-decoration: line-through;
+ }
}
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
index 3240357c254eac..b1cc4c06c2165a 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
@@ -15,7 +15,7 @@ describe('DragDrop', () => {
test('renders if nothing is being dragged', () => {
const component = render(
- Hello!
+
);
@@ -24,7 +24,11 @@ describe('DragDrop', () => {
test('dragover calls preventDefault if droppable is true', () => {
const preventDefault = jest.fn();
- const component = mount(
Hello!);
+ const component = mount(
+
+
+
+ );
component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault });
@@ -33,7 +37,11 @@ describe('DragDrop', () => {
test('dragover does not call preventDefault if droppable is false', () => {
const preventDefault = jest.fn();
- const component = mount(
Hello!);
+ const component = mount(
+
+
+
+ );
component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault });
@@ -51,7 +59,7 @@ describe('DragDrop', () => {
const component = mount(
- Hello!
+
);
@@ -74,7 +82,7 @@ describe('DragDrop', () => {
const component = mount(
- Hello!
+
);
@@ -98,7 +106,7 @@ describe('DragDrop', () => {
const component = mount(
- Hello!
+
);
@@ -121,7 +129,7 @@ describe('DragDrop', () => {
}}
droppable
>
- Hello!
+
);
@@ -132,10 +140,10 @@ describe('DragDrop', () => {
const component = mount(
{}}>
- Ignored
+
{}} droppable={false}>
- Hello!
+
);
@@ -154,14 +162,14 @@ describe('DragDrop', () => {
}}
>
- Ignored
+
{}}
droppable
getAdditionalClassesOnEnter={getAdditionalClasses}
>
- Hello!
+
);
diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
index 6941974a63cd3f..b36415fee5b152 100644
--- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
+++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
@@ -41,9 +41,14 @@ interface BaseProps {
value?: unknown;
/**
- * The React children.
+ * Optional comparison function to check whether a value is the dragged one
*/
- children: React.ReactNode;
+ isValueEqual?: (value1: unknown, value2: unknown) => boolean;
+
+ /**
+ * The React element which will be passed the draggable handlers
+ */
+ children: React.ReactElement;
/**
* Indicates whether or not the currently dragged item
@@ -60,6 +65,18 @@ interface BaseProps {
* The optional test subject associated with this DOM element.
*/
'data-test-subj'?: string;
+
+ /**
+ * Indicates to the user whether the currently dragged item
+ * will be moved or copied
+ */
+ dragType?: 'copy' | 'move';
+
+ /**
+ * Indicates to the user whether the drop action will
+ * replace something that is existing or add a new one
+ */
+ dropType?: 'add' | 'replace';
}
/**
@@ -98,12 +115,14 @@ type Props = DraggableProps | NonDraggableProps;
export const DragDrop = (props: Props) => {
const { dragging, setDragging } = useContext(DragContext);
- const { value, draggable, droppable } = props;
+ const { value, draggable, droppable, isValueEqual } = props;
return (
- {children}
-
- );
+ return React.cloneElement(children, {
+ 'data-test-subj': props['data-test-subj'] || 'lnsDragDrop',
+ className: classNames(children.props.className, classes),
+ onDragOver: dragOver,
+ onDragLeave: dragLeave,
+ onDrop: drop,
+ draggable,
+ onDragEnd: dragEnd,
+ onDragStart: dragStart,
+ });
});
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.scss
index 1965b51f970345..a58b5c21e77242 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.scss
@@ -3,5 +3,5 @@
// Remove EuiButton's default shadow to make button more subtle
// sass-lint:disable-block no-important
box-shadow: none !important;
- border: 1px dashed currentColor;
+ border-color: $euiColorLightShade;
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
index ad16038f44911c..6b7e5ba8ea89d8 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx
@@ -121,6 +121,9 @@ function LayerPanels(
{
dispatch({
type: 'UPDATE_STATE',
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
index a415eb44cf196a..19f4c0428260e1 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
@@ -130,7 +130,7 @@ export function DimensionContainer({
return (
<>
- {trigger}
+ {trigger}
{flyout}
>
);
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss
index b85c3e843613db..c77db2e65ce2d7 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss
@@ -27,36 +27,41 @@
.lnsLayerPanel__dimension {
@include euiFontSizeS;
- background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade);
border-radius: $euiBorderRadius;
display: flex;
align-items: center;
margin-top: $euiSizeXS;
overflow: hidden;
-}
+ width: 100%;
+ min-height: $euiSizeXXL;
-.lnsLayerPanel__dimension-isHidden {
- opacity: 0;
-}
+ // NativeRenderer is messing this up
+ > div {
+ flex-grow: 1;
+ }
-.lnsLayerPanel__dimension-isReplacing {
- text-decoration: line-through;
+ &:focus,
+ &:focus-within {
+ @include euiFocusRing;
+ }
}
.lnsLayerPanel__triggerLink {
- padding: $euiSizeS;
width: 100%;
- display: flex;
- align-items: center;
- min-height: $euiSizeXXL;
-}
+ padding: $euiSizeS;
+ min-height: $euiSizeXXL - 2;
-.lnsLayerPanel__anchor {
- width: 100%;
+ &:focus {
+ background-color: transparent !important; // sass-lint:disable-line no-important
+ outline: none !important; // sass-lint:disable-line no-important
+ }
}
-.lnsLayerPanel__dndGrab {
- padding: $euiSizeS;
+.lnsLayerPanel__triggerLinkContent {
+ // Make EUI button content not centered
+ justify-content: flex-start;
+ padding: 0 !important; // sass-lint:disable-line no-important
+ color: $euiTextSubduedColor;
}
.lnsLayerPanel__styleEditor {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index 46cd0292f2459a..ce2955da890d7c 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -17,7 +17,6 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import classNames from 'classnames';
import { NativeRenderer } from '../../../native_renderer';
import { StateSetter, isDraggedOperation } from '../../../types';
import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop';
@@ -33,6 +32,28 @@ const initialDimensionContainerState = {
addingToGroupId: null,
};
+function isConfiguration(
+ value: unknown
+): value is { columnId: string; groupId: string; layerId: string } {
+ return (
+ value &&
+ typeof value === 'object' &&
+ 'columnId' in value &&
+ 'groupId' in value &&
+ 'layerId' in value
+ );
+}
+
+function isSameConfiguration(config1: unknown, config2: unknown) {
+ return (
+ isConfiguration(config1) &&
+ isConfiguration(config2) &&
+ config1.columnId === config2.columnId &&
+ config1.groupId === config2.groupId &&
+ config1.layerId === config2.layerId
+ );
+}
+
export function LayerPanel(
props: Exclude & {
layerId: string;
@@ -203,25 +224,22 @@ export function LayerPanel(
return (
{
- // If we are dragging another column, add an indication that the behavior will be a replacement'
- if (
- isDraggedOperation(dragDropContext.dragging) &&
- group.groupId !== dragDropContext.dragging.groupId
- ) {
- return 'lnsLayerPanel__dimension-isReplacing';
- }
- return '';
- }}
+ dragType={
+ isDraggedOperation(dragDropContext.dragging) &&
+ accessor === dragDropContext.dragging.columnId
+ ? 'move'
+ : 'copy'
+ }
+ dropType={
+ isDraggedOperation(dragDropContext.dragging) &&
+ group.groupId !== dragDropContext.dragging.groupId
+ ? 'replace'
+ : 'add'
+ }
data-test-subj={group.dataTestSubj}
draggable={!dimensionContainerState.isOpen}
value={{ columnId: accessor, groupId: group.groupId, layerId }}
+ isValueEqual={isSameConfiguration}
label={group.groupLabel}
droppable={
Boolean(dragDropContext.dragging) &&
@@ -254,83 +272,84 @@ export function LayerPanel(
}
}}
>
- {
- if (dimensionContainerState.isOpen) {
- setDimensionContainerState(initialDimensionContainerState);
- } else {
- setDimensionContainerState({
- isOpen: true,
- openId: accessor,
- addingToGroupId: null, // not set for existing dimension
- });
- }
- },
- }}
- />
- }
- panel={
- <>
- {datasourceDimensionEditor}
- {visDimensionEditor}
- >
- }
- panelTitle={i18n.translate('xpack.lens.configure.configurePanelTitle', {
- defaultMessage: '{groupLabel} configuration',
- values: {
- groupLabel: group.groupLabel,
- },
- })}
- />
+
+ {
+ if (dimensionContainerState.isOpen) {
+ setDimensionContainerState(initialDimensionContainerState);
+ } else {
+ setDimensionContainerState({
+ isOpen: true,
+ openId: accessor,
+ addingToGroupId: null, // not set for existing dimension
+ });
+ }
+ },
+ }}
+ />
+ }
+ panel={
+ <>
+ {datasourceDimensionEditor}
+ {visDimensionEditor}
+ >
+ }
+ panelTitle={i18n.translate('xpack.lens.configure.configurePanelTitle', {
+ defaultMessage: '{groupLabel} configuration',
+ values: {
+ groupLabel: group.groupLabel,
+ },
+ })}
+ />
- {
- trackUiEvent('indexpattern_dimension_removed');
- props.updateAll(
- datasourceId,
- layerDatasource.removeColumn({
- layerId,
- columnId: accessor,
- prevState: layerDatasourceState,
- }),
- activeVisualization.removeDimension({
- layerId,
- columnId: accessor,
- prevState: props.visualizationState,
- })
- );
- }}
- />
+ {
+ trackUiEvent('indexpattern_dimension_removed');
+ props.updateAll(
+ datasourceId,
+ layerDatasource.removeColumn({
+ layerId,
+ columnId: accessor,
+ prevState: layerDatasourceState,
+ }),
+ activeVisualization.removeDimension({
+ layerId,
+ columnId: accessor,
+ prevState: props.visualizationState,
+ })
+ );
+ }}
+ />
+
);
})}
{group.supportsMoreColumns ? (
-
+
+ {
if (dimensionContainerState.isOpen) {
setDimensionContainerState(initialDimensionContainerState);
@@ -402,52 +421,51 @@ export function LayerPanel(
});
}
}}
- size="xs"
>
-
- }
- panelTitle={i18n.translate('xpack.lens.configure.configurePanelTitle', {
- defaultMessage: '{groupLabel} configuration',
- values: {
- groupLabel: group.groupLabel,
- },
- })}
- panel={
- {
- props.updateAll(
- datasourceId,
- newState,
- activeVisualization.setDimension({
- layerId,
- groupId: group.groupId,
- columnId: newId,
- prevState: props.visualizationState,
- })
- );
- setDimensionContainerState({
- isOpen: true,
- openId: newId,
- addingToGroupId: null, // clear now that dimension exists
- });
- },
- }}
- />
- }
- />
+ setState: (newState: unknown) => {
+ props.updateAll(
+ datasourceId,
+ newState,
+ activeVisualization.setDimension({
+ layerId,
+ groupId: group.groupId,
+ columnId: newId,
+ prevState: props.visualizationState,
+ })
+ );
+ setDimensionContainerState({
+ isOpen: true,
+ openId: newId,
+ addingToGroupId: null, // clear now that dimension exists
+ });
+ },
+ }}
+ />
+ }
+ />
+
) : null}
>
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss
index bad0563f16f1fd..ac52190dc7b0d6 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss
@@ -20,7 +20,7 @@
.lnsFrameLayout__pageBody {
@include euiScrollBar;
min-width: $lnsPanelMinWidth + $euiSizeXL;
- overflow: hidden;
+ overflow: hidden auto;
// Leave out bottom padding so the suggestions scrollbar stays flush to window edge
// Leave out left padding so the left sidebar's focus states are visible outside of content bounds
// This also means needing to add same amount of margin to page content and suggestion items
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index e56e55fdd5d6c4..2a5798ac6a70c0 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
@@ -221,7 +221,7 @@ export function InnerWorkspacePanel({
)}
-
+
{expression === null && (
<>
@@ -257,7 +257,7 @@ export function InnerWorkspacePanel({
if (localState.expressionBuildError) {
return (
-
+
@@ -283,7 +283,7 @@ export function InnerWorkspacePanel({
onEvent={onEvent}
renderError={(errorMessage?: string | null) => {
return (
-
+
@@ -338,8 +338,10 @@ export function InnerWorkspacePanel({
droppable={Boolean(suggestionForDraggedField)}
onDrop={onDrop}
>
- {renderVisualization()}
- {Boolean(suggestionForDraggedField) && expression !== null && renderEmptyWorkspace()}
+
+ {renderVisualization()}
+ {Boolean(suggestionForDraggedField) && expression !== null && renderEmptyWorkspace()}
+
);
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
index 7f7385f029ed4e..33b9b2fe1dbf0b 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss
@@ -1,3 +1,5 @@
+@import '../../../mixins';
+
.lnsWorkspacePanelWrapper {
@include euiScrollBar;
overflow: hidden;
@@ -7,6 +9,7 @@
display: flex;
flex-direction: column;
position: relative; // For positioning the dnd overlay
+ min-height: $euiSizeXXL * 10;
.lnsWorkspacePanelWrapper__pageContentHeader {
@include euiTitle('xs');
@@ -25,7 +28,7 @@
display: flex;
align-items: stretch;
justify-content: stretch;
- overflow: hidden;
+ overflow: auto;
> * {
flex: 1 1 100%;
@@ -41,6 +44,7 @@
// Disable the coloring of the DnD for this element as we'll
// Color the whole panel instead
background-color: transparent !important; // sass-lint:disable-line no-important
+ border: none !important; // sass-lint:disable-line no-important
}
.lnsExpressionRenderer {
@@ -60,28 +64,25 @@
display: flex;
justify-content: center;
align-items: center;
- transition: background-color $euiAnimSpeedNormal ease-in-out;
+ transition: background-color $euiAnimSpeedFast ease-in-out;
.lnsDragDrop-isDropTarget & {
- background-color: transparentize($euiColorSecondary, .9);
+ @include lnsDroppable;
+ @include lnsDroppableActive;
p {
- transition: filter $euiAnimSpeedNormal ease-in-out;
+ transition: filter $euiAnimSpeedFast ease-in-out;
filter: blur(5px);
}
}
.lnsDragDrop-isActiveDropTarget & {
- background-color: transparentize($euiColorSecondary, .75);
+ @include lnsDroppableActiveHover;
.lnsDropIllustration__hand {
- animation: pulseArrowContinuous 1.5s ease-in-out 0s infinite normal forwards;
+ animation: lnsWorkspacePanel__illustrationPulseContinuous 1.5s ease-in-out 0s infinite normal forwards;
}
}
-
- &.lnsWorkspacePanel__emptyContent-onTop p {
- display: none;
- }
}
.lnsWorkspacePanelWrapper__toolbar {
@@ -106,10 +107,10 @@
}
.lnsDropIllustration__hand {
- animation: pulseArrow 5s ease-in-out 0s infinite normal forwards;
+ animation: lnsWorkspacePanel__illustrationPulseArrow 5s ease-in-out 0s infinite normal forwards;
}
-@keyframes pulseArrow {
+@keyframes lnsWorkspacePanel__illustrationPulseArrow {
0% { transform: translateY(0%); }
65% { transform: translateY(0%); }
72% { transform: translateY(10%); }
@@ -118,7 +119,7 @@
95% { transform: translateY(0); }
}
-@keyframes pulseArrowContinuous {
+@keyframes lnsWorkspacePanel__illustrationPulseContinuous {
0% { transform: translateY(10%); }
25% { transform: translateY(15%); }
50% { transform: translateY(10%); }
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
index 155b954e9cf170..df73789eadedfd 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
@@ -12,7 +12,7 @@
.lnsInnerIndexPatternDataPanel__fieldItems {
// Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds
- padding: $euiSizeXS $euiSizeXS 0;
+ padding: $euiSizeXS;
}
.lnsInnerIndexPatternDataPanel__textField {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
index 6f0a9c2a86acd3..12b8d91c35ade9 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx
@@ -250,6 +250,10 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens
return null;
}
+ const triggerLinkA11yText = i18n.translate('xpack.lens.configure.editConfig', {
+ defaultMessage: 'Click to edit configuration or drag to move',
+ });
+
if (currentFieldIsInvalid) {
return (
}
- anchorClassName="lnsLayerPanel__anchor"
+ anchorClassName="eui-displayBlock"
>
@@ -296,12 +296,8 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens
className="lnsLayerPanel__triggerLink"
onClick={props.onClick}
data-test-subj="lns-dimensionTrigger"
- aria-label={i18n.translate('xpack.lens.configure.editConfig', {
- defaultMessage: 'Edit configuration',
- })}
- title={i18n.translate('xpack.lens.configure.editConfig', {
- defaultMessage: 'Edit configuration',
- })}
+ aria-label={triggerLinkA11yText}
+ title={triggerLinkA11yText}
>
{uniqueLabel}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss
index d74c332dd42e56..1b55d9623e2237 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.scss
@@ -1,25 +1,25 @@
-.lnsFieldItem--missing {
- .lnsFieldItem__info {
- background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade);
- color: $euiColorDarkShade;
- }
-}
-
-.lnsFieldItem__info {
+.lnsFieldItem {
.lnsFieldItem__infoIcon {
visibility: hidden;
+ opacity: 0;
}
- &:hover,
- &:focus {
+ &:hover:not([class*='isActive']) {
cursor: grab;
.lnsFieldItem__infoIcon {
visibility: visible;
+ opacity: 1;
+ transition: opacity $euiAnimSpeedFast ease-in-out 1s;
}
}
}
+.lnsFieldItem--missing {
+ background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade);
+ color: $euiColorDarkShade;
+}
+
.lnsFieldItem__topValue {
margin-bottom: $euiSizeS;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index 7377d15bca6d73..2fbe23f9085f2c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -33,6 +33,7 @@ import {
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { DataPublicPluginStart } from 'src/plugins/data/public';
+import { EuiHighlight } from '@elastic/eui';
import {
Query,
KBN_FIELD_TYPES,
@@ -102,22 +103,6 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
isLoading: false,
});
- const wrappableName = wrapOnDot(field.displayName)!;
- const wrappableHighlight = wrapOnDot(highlight);
- const highlightIndex = wrappableHighlight
- ? wrappableName.toLowerCase().indexOf(wrappableHighlight.toLowerCase())
- : -1;
- const wrappableHighlightableFieldName =
- highlightIndex < 0 ? (
- wrappableName
- ) : (
-
- {wrappableName.substr(0, highlightIndex)}
- {wrappableName.substr(highlightIndex, wrappableHighlight.length)}
- {wrappableName.substr(highlightIndex + wrappableHighlight.length)}
-
- );
-
function fetchData() {
if (state.isLoading) {
return;
@@ -200,22 +185,20 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
ownFocus
className="lnsFieldItem__popoverAnchor"
display="block"
+ data-test-subj="lnsFieldListPanelField"
container={document.querySelector('.application') || undefined}
button={
+ {wrapOnDot(field.displayName)}
+
+ }
fieldInfoIcon={lensInfoIcon}
/>
@@ -527,7 +514,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
-
+
{Math.round((otherCount / props.sampledValues!) * 100)}%
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx
index 47380f7865578c..50471ca84c0d83 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx
@@ -96,7 +96,13 @@ export const DraggableBucketContainer = ({
children: React.ReactNode;
} & BucketContainerProps) => {
return (
-
+
{(provided) => {children}}
);
@@ -134,7 +140,7 @@ export const DragDropBuckets = ({
};
return (
-
+
{children}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index ba9d8e364bd173..34c339023171e1 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9464,7 +9464,6 @@
"xpack.lens.configPanel.color.tooltip.custom": "[自動]モードに戻すには、カスタム色をオフにしてください。",
"xpack.lens.configPanel.color.tooltip.disabled": "レイヤーに「内訳条件」が含まれている場合は、個別の系列をカスタム色にできません。",
"xpack.lens.configPanel.selectVisualization": "ビジュアライゼーションを選択してください",
- "xpack.lens.configure.addConfig": "構成を追加",
"xpack.lens.configure.editConfig": "構成の編集",
"xpack.lens.configure.emptyConfig": "ここにフィールドをドロップ",
"xpack.lens.dataPanelWrapper.switchDatasource": "データソースに切り替える",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 5d44e0c635bee0..f32b49fd4f2f00 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9470,7 +9470,6 @@
"xpack.lens.configPanel.color.tooltip.custom": "清除定制颜色以返回到“自动”模式。",
"xpack.lens.configPanel.color.tooltip.disabled": "当图层包括“细分依据”,各个系列无法定制颜色。",
"xpack.lens.configPanel.selectVisualization": "选择可视化",
- "xpack.lens.configure.addConfig": "添加配置",
"xpack.lens.configure.editConfig": "编辑配置",
"xpack.lens.configure.emptyConfig": "将字段拖放到此处",
"xpack.lens.dataPanelWrapper.switchDatasource": "切换到数据源",
From 198c5d998816e940e4c8d92979504c86116bcd2d Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Thu, 1 Oct 2020 18:02:37 +0200
Subject: [PATCH 039/128] [Lens] Fix embeddable title and description for
reporting and dashboard tooltip (#78767)
Co-authored-by: Elastic Machine
---
.../lib/panel/panel_header/panel_header.tsx | 9 ++-
.../public/embeddable/visualize_embeddable.ts | 2 +-
.../__snapshots__/expression.test.tsx.snap | 4 +-
.../datatable_visualization/expression.tsx | 10 ++-
.../datatable_visualization/visualization.tsx | 4 +-
.../editor_frame/expression_helpers.ts | 9 ++-
.../editor_frame/state_helpers.ts | 4 ++
.../embeddable/embeddable.tsx | 6 ++
.../metric_visualization/expression.test.tsx | 69 +++++++++++++++++--
.../metric_visualization/expression.tsx | 24 +++++--
.../lens/public/metric_visualization/types.ts | 2 +
.../visualization.test.ts | 8 ++-
.../metric_visualization/visualization.tsx | 10 +--
.../public/pie_visualization/expression.tsx | 8 +++
.../pie_visualization/render_function.tsx | 7 +-
.../public/pie_visualization/to_expression.ts | 13 ++--
.../lens/public/pie_visualization/types.ts | 2 +
x-pack/plugins/lens/public/types.ts | 3 +-
.../public/visualization_container.test.tsx | 7 +-
.../lens/public/visualization_container.tsx | 11 ++-
.../__snapshots__/to_expression.test.ts.snap | 6 ++
.../public/xy_visualization/expression.tsx | 15 +++-
.../public/xy_visualization/to_expression.ts | 10 ++-
.../lens/public/xy_visualization/types.ts | 2 +
24 files changed, 206 insertions(+), 39 deletions(-)
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
index 7dde84e510535e..dea483efb349f9 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx
@@ -109,12 +109,11 @@ function renderTooltip(description: string) {
);
}
-const VISUALIZE_EMBEDDABLE_TYPE = 'visualization';
-type VisualizeEmbeddable = any;
+type EmbeddableWithDescription = IEmbeddable & { getDescription: () => string };
-function getViewDescription(embeddable: IEmbeddable | VisualizeEmbeddable) {
- if (embeddable.type === VISUALIZE_EMBEDDABLE_TYPE) {
- const description = embeddable.getVisualizationDescription();
+function getViewDescription(embeddable: IEmbeddable | EmbeddableWithDescription) {
+ if ('getDescription' in embeddable) {
+ const description = embeddable.getDescription();
if (description) {
return description;
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index c091d396b49244..fe8a9adff40529 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -167,7 +167,7 @@ export class VisualizeEmbeddable
typeof inspectorAdapters === 'function' ? inspectorAdapters() : inspectorAdapters;
}
}
- public getVisualizationDescription() {
+ public getDescription() {
return this.vis.description;
}
diff --git a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap
index c0210c3915ce83..9c7bdc3397f9cb 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap
@@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`datatable_expression DatatableComponent it renders the title and value 1`] = `
-
+
+
};
},
- toExpression(state, datasourceLayers): Ast {
+ toExpression(state, datasourceLayers, { title, description } = {}): Ast {
const layer = state.layers[0];
const datasource = datasourceLayers[layer.layerId];
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
@@ -211,6 +211,8 @@ export const datatableVisualization: Visualization
type: 'function',
function: 'lens_datatable',
arguments: {
+ title: [title || ''],
+ description: [description || ''],
columns: [
{
type: 'expression',
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts
index 952718e13c8cfd..e7568147dc568b 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts
@@ -73,7 +73,11 @@ export function buildExpression({
datasourceMap,
datasourceStates,
datasourceLayers,
+ title,
+ description,
}: {
+ title?: string;
+ description?: string;
visualization: Visualization | null;
visualizationState: unknown;
datasourceMap: Record;
@@ -89,7 +93,10 @@ export function buildExpression({
if (visualization === null) {
return null;
}
- const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers);
+ const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers, {
+ title,
+ description,
+ });
const completeExpression = prependDatasourceExpression(
visualizationExpression,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
index 6deb9ffd37a06f..1fe5224d0b1b4e 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
@@ -61,6 +61,8 @@ export async function persistedStateToExpression(
state: { visualization: visualizationState, datasourceStates: persistedDatasourceStates },
visualizationType,
references,
+ title,
+ description,
} = doc;
if (!visualizationType) return null;
const visualization = visualizations[visualizationType!];
@@ -78,6 +80,8 @@ export async function persistedStateToExpression(
const datasourceLayers = createDatasourceLayers(datasources, datasourceStates);
return buildExpression({
+ title,
+ description,
visualization,
visualizationState,
datasourceMap: datasources,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
index 61a5d8cacdc4f3..16b19ca0af8497 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
@@ -295,6 +295,12 @@ export class Embeddable
return this.deps.attributeService.getInputAsValueType(input);
};
+ // same API as Visualize
+ public getDescription() {
+ // mind that savedViz is loaded in async way here
+ return this.savedVis && this.savedVis.description;
+ }
+
destroy() {
super.destroy();
if (this.domNode) {
diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
index 0c92cdb2c31fcc..77a8ce64b21a28 100644
--- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
@@ -32,10 +32,21 @@ function sampleArgs() {
accessor: 'a',
layerId: 'l1',
title: 'My fanci metric chart',
+ description: 'Fancy chart description',
+ metricTitle: 'My fanci metric chart',
mode: 'full',
};
- return { data, args };
+ const noAttributesArgs: MetricConfig = {
+ accessor: 'a',
+ layerId: 'l1',
+ title: '',
+ description: '',
+ metricTitle: 'My fanci metric chart',
+ mode: 'full',
+ };
+
+ return { data, args, noAttributesArgs };
}
describe('metric_expression', () => {
@@ -53,7 +64,7 @@ describe('metric_expression', () => {
});
describe('MetricChart component', () => {
- test('it renders the title and value', () => {
+ test('it renders the all attributes when passed (title, description, metricTitle, value)', () => {
const { data, args } = sampleArgs();
expect(
@@ -61,6 +72,7 @@ describe('metric_expression', () => {
).toMatchInlineSnapshot(`
@@ -90,21 +102,66 @@ describe('metric_expression', () => {
`);
});
- test('it does not render title in reduced mode', () => {
- const { data, args } = sampleArgs();
+ test('it renders only chart content when title and description are empty strings', () => {
+ const { data, noAttributesArgs } = sampleArgs();
expect(
shallow(
x as IFieldFormat}
/>
)
).toMatchInlineSnapshot(`
+
+
+ 10110
+
+
+ My fanci metric chart
+
+
+
+ `);
+ });
+
+ test('it does not render metricTitle in reduced mode', () => {
+ const { data, noAttributesArgs } = sampleArgs();
+
+ expect(
+ shallow(
+ x as IFieldFormat}
+ />
+ )
+ ).toMatchInlineSnapshot(`
+
+
);
}
@@ -119,14 +131,18 @@ export function MetricChart({
}
return (
-
+
{value}
{mode === 'full' && (
- {title}
+ {metricTitle}
)}
diff --git a/x-pack/plugins/lens/public/metric_visualization/types.ts b/x-pack/plugins/lens/public/metric_visualization/types.ts
index 86a781716b3457..c4a3fd094abe61 100644
--- a/x-pack/plugins/lens/public/metric_visualization/types.ts
+++ b/x-pack/plugins/lens/public/metric_visualization/types.ts
@@ -11,5 +11,7 @@ export interface State {
export interface MetricConfig extends State {
title: string;
+ description: string;
+ metricTitle: string;
mode: 'reduced' | 'full';
}
diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts
index aa3de93013e66b..80c7a174b32646 100644
--- a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts
+++ b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts
@@ -171,11 +171,17 @@ describe('metric_visualization', () => {
"accessor": Array [
"a",
],
+ "description": Array [
+ "",
+ ],
+ "metricTitle": Array [
+ "shazm",
+ ],
"mode": Array [
"full",
],
"title": Array [
- "shazm",
+ "",
],
},
"function": "lens_metric_chart",
diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
index 72c07bed1acb22..77d189ce53d012 100644
--- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx
@@ -14,7 +14,7 @@ import { State } from './types';
const toExpression = (
state: State,
datasourceLayers: Record,
- mode: 'reduced' | 'full' = 'full'
+ attributes?: { mode?: 'reduced' | 'full'; title?: string; description?: string }
): Ast | null => {
if (!state.accessor) {
return null;
@@ -30,9 +30,11 @@ const toExpression = (
type: 'function',
function: 'lens_metric_chart',
arguments: {
- title: [(operation && operation.label) || ''],
+ title: [attributes?.title || ''],
+ description: [attributes?.description || ''],
+ metricTitle: [(operation && operation.label) || ''],
accessor: [state.accessor],
- mode: [mode],
+ mode: [attributes?.mode || 'full'],
},
},
],
@@ -104,7 +106,7 @@ export const metricVisualization: Visualization = {
toExpression,
toPreviewExpression: (state, datasourceLayers) =>
- toExpression(state, datasourceLayers, 'reduced'),
+ toExpression(state, datasourceLayers, { mode: 'reduced' }),
setDimension({ prevState, columnId }) {
return { ...prevState, accessor: columnId };
diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx
index 89d93ab79233f7..d93145f29aa89a 100644
--- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx
@@ -37,6 +37,14 @@ export const pie: ExpressionFunctionDefinition<
defaultMessage: 'Pie renderer',
}),
args: {
+ title: {
+ types: ['string'],
+ help: 'The chart title.',
+ },
+ description: {
+ types: ['string'],
+ help: '',
+ },
groups: {
types: ['string'],
multi: true,
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index d97ab146e000d7..8de810f9aa5d3f 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -228,7 +228,12 @@ export function PieComponent(
);
}
return (
-
+
+ datasourceLayers: Record,
+ attributes: Partial<{ title: string; description: string }> = {}
) {
- return expressionHelper(state, datasourceLayers, false);
+ return expressionHelper(state, datasourceLayers, { ...attributes, isPreview: false });
}
function expressionHelper(
state: PieVisualizationState,
datasourceLayers: Record,
- isPreview: boolean
+ attributes: { isPreview: boolean; title?: string; description?: string } = { isPreview: false }
): Ast | null {
const layer = state.layers[0];
const datasource = datasourceLayers[layer.layerId];
@@ -37,8 +38,10 @@ function expressionHelper(
type: 'function',
function: 'lens_pie',
arguments: {
+ title: [attributes.title || ''],
+ description: [attributes.description || ''],
shape: [state.shape],
- hideLabels: [isPreview],
+ hideLabels: [attributes.isPreview],
groups: operations.map((o) => o.columnId),
metric: [layer.metric],
numberDisplay: [layer.numberDisplay],
@@ -57,5 +60,5 @@ export function toPreviewExpression(
state: PieVisualizationState,
datasourceLayers: Record
) {
- return expressionHelper(state, datasourceLayers, true);
+ return expressionHelper(state, datasourceLayers, { isPreview: true });
}
diff --git a/x-pack/plugins/lens/public/pie_visualization/types.ts b/x-pack/plugins/lens/public/pie_visualization/types.ts
index 74156bce2ea703..603c80aa00066e 100644
--- a/x-pack/plugins/lens/public/pie_visualization/types.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/types.ts
@@ -28,6 +28,8 @@ export interface PieVisualizationState {
}
export type PieExpressionArgs = SharedLayerState & {
+ title?: string;
+ description?: string;
shape: 'pie' | 'donut' | 'treemap';
hideLabels: boolean;
};
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index e97e0d612a2ee6..e5e8a645dd0e80 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -519,7 +519,8 @@ export interface Visualization {
toExpression: (
state: T,
- datasourceLayers: Record
+ datasourceLayers: Record,
+ attributes?: Partial<{ title: string; description: string }>
) => Ast | string | null;
/**
* Expression to render a preview version of the chart in very constrained space.
diff --git a/x-pack/plugins/lens/public/visualization_container.test.tsx b/x-pack/plugins/lens/public/visualization_container.test.tsx
index 454399ec901215..7fc3541c63072b 100644
--- a/x-pack/plugins/lens/public/visualization_container.test.tsx
+++ b/x-pack/plugins/lens/public/visualization_container.test.tsx
@@ -43,13 +43,16 @@ describe('VisualizationContainer', () => {
expect(reportingEl.prop('data-shared-item')).toBeTruthy();
});
- test('renders title for reporting, if provided', () => {
+ test('renders title and description for reporting, if provided', () => {
const component = mount(
- Hello!
+
+ Hello!
+
);
const reportingEl = component.find('[data-shared-item]').first();
expect(reportingEl.prop('data-title')).toEqual('shazam!');
+ expect(reportingEl.prop('data-description')).toEqual('Description');
});
test('renders style', () => {
diff --git a/x-pack/plugins/lens/public/visualization_container.tsx b/x-pack/plugins/lens/public/visualization_container.tsx
index 521d41b6f8d947..429dc63231cbee 100644
--- a/x-pack/plugins/lens/public/visualization_container.tsx
+++ b/x-pack/plugins/lens/public/visualization_container.tsx
@@ -12,6 +12,7 @@ import classNames from 'classnames';
interface Props extends React.HTMLAttributes {
isReady?: boolean;
reportTitle?: string;
+ reportDescription?: string;
}
/**
@@ -21,16 +22,24 @@ interface Props extends React.HTMLAttributes {
export function VisualizationContainer({
isReady = true,
reportTitle,
+ reportDescription,
children,
className,
...rest
}: Props) {
+ const attributes: Partial<{ 'data-title': string; 'data-description': string }> = {};
+ if (reportTitle) {
+ attributes['data-title'] = reportTitle;
+ }
+ if (reportDescription) {
+ attributes['data-description'] = reportDescription;
+ }
return (
{children}
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
index dd8c6377cacdc1..b35f915336eeed 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
@@ -27,6 +27,9 @@ Object {
"type": "expression",
},
],
+ "description": Array [
+ "",
+ ],
"fittingFunction": Array [
"Carry",
],
@@ -139,6 +142,9 @@ Object {
"type": "expression",
},
],
+ "title": Array [
+ "",
+ ],
"xTitle": Array [
"",
],
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
index 64e0a3670a9aae..f36525a9a0b146 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
@@ -85,6 +85,14 @@ export const xyChart: ExpressionFunctionDefinition<
defaultMessage: 'An X/Y chart',
}),
args: {
+ title: {
+ types: ['string'],
+ help: 'The chart title.',
+ },
+ description: {
+ types: ['string'],
+ help: '',
+ },
xTitle: {
types: ['string'],
help: i18n.translate('xpack.lens.xyChart.xTitle.help', {
@@ -215,7 +223,12 @@ export function XYChartReportable(props: XYChartRenderProps) {
}, [setState]);
return (
-
+
);
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
index df8d571a1fdf85..5a3c8faa2d4762 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
@@ -15,7 +15,8 @@ interface ValidLayer extends LayerConfig {
export const toExpression = (
state: State,
- datasourceLayers: Record
+ datasourceLayers: Record,
+ attributes: Partial<{ title: string; description: string }> = {}
): Ast | null => {
if (!state || !state.layers.length) {
return null;
@@ -31,7 +32,7 @@ export const toExpression = (
});
});
- return buildExpression(state, metadata, datasourceLayers);
+ return buildExpression(state, metadata, datasourceLayers, attributes);
};
export function toPreviewExpression(
@@ -81,7 +82,8 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S
export const buildExpression = (
state: State,
metadata: Record>,
- datasourceLayers?: Record
+ datasourceLayers?: Record,
+ attributes: Partial<{ title: string; description: string }> = {}
): Ast | null => {
const validLayers = state.layers.filter((layer): layer is ValidLayer =>
Boolean(layer.accessors.length)
@@ -97,6 +99,8 @@ export const buildExpression = (
type: 'function',
function: 'lens_xy_chart',
arguments: {
+ title: [attributes?.title || ''],
+ description: [attributes?.description || ''],
xTitle: [state.xTitle || ''],
yTitle: [state.yTitle || ''],
yRightTitle: [state.yRightTitle || ''],
diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts
index 185fa20f169eec..cac982f852c7a4 100644
--- a/x-pack/plugins/lens/public/xy_visualization/types.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/types.ts
@@ -383,6 +383,8 @@ export type LayerArgs = LayerConfig & {
// Arguments to XY chart expression, with computed properties
export interface XYArgs {
+ title?: string;
+ description?: string;
xTitle: string;
yTitle: string;
yRightTitle: string;
From a61f4d4cbfd08dfe2502cebd059464bf3df75707 Mon Sep 17 00:00:00 2001
From: Chris Roberson
Date: Thu, 1 Oct 2020 12:28:34 -0400
Subject: [PATCH 040/128] [Monitoring] Missing data alert (#78208)
* WIP for alert
* Surface alert most places
* Fix up alert placement
* Fix tests
* Type fix
* Update copy
* Add alert presence to APM in the UI
* Fetch data a little differently
* We don't need moment
* Add tests
* PR feedback
* Update copy
* Fix up bug around grabbing old data
* PR feedback
* PR feedback
* Fix tests
---
x-pack/plugins/monitoring/common/constants.ts | 2 +
x-pack/plugins/monitoring/common/types.ts | 6 +-
.../monitoring/public/alerts/badge.tsx | 15 +-
.../monitoring/public/alerts/callout.tsx | 11 +-
.../public/alerts/filter_alert_states.ts | 23 +
.../expression.tsx | 61 +++
.../missing_monitoring_data_alert/index.ts | 7 +
.../missing_monitoring_data_alert.tsx | 28 +
.../validation.tsx | 35 ++
.../monitoring/public/alerts/panel.tsx | 10 +-
.../monitoring/public/alerts/status.tsx | 15 +-
.../components/apm/instance/instance.js | 14 +-
.../public/components/apm/instance/status.js | 3 +-
.../components/apm/instances/instances.js | 32 +-
.../public/components/apm/instances/status.js | 3 +-
.../public/components/apm/overview/index.js | 4 +-
.../public/components/beats/beat/beat.js | 18 +-
.../components/beats/listing/listing.js | 29 +-
.../components/beats/overview/overview.js | 3 +-
.../public/components/beats/stats.js | 4 +-
.../components/cluster/overview/apm_panel.js | 29 +-
.../cluster/overview/beats_panel.js | 25 +-
.../cluster/overview/elasticsearch_panel.js | 2 +
.../components/cluster/overview/index.js | 54 +-
.../cluster/overview/kibana_panel.js | 8 +-
.../cluster/overview/logstash_panel.js | 3 +-
.../components/elasticsearch/node/node.js | 15 +-
.../components/elasticsearch/nodes/nodes.js | 10 +-
.../components/kibana/instances/instances.js | 13 +-
.../components/logstash/listing/listing.js | 13 +-
x-pack/plugins/monitoring/public/plugin.ts | 2 +
.../public/views/apm/instance/index.js | 18 +-
.../public/views/apm/instances/index.js | 18 +-
.../public/views/apm/overview/index.js | 24 +-
.../public/views/beats/beat/index.js | 18 +-
.../public/views/beats/listing/index.js | 18 +-
.../public/views/beats/overview/index.js | 24 +-
.../elasticsearch/node/advanced/index.js | 3 +-
.../public/views/elasticsearch/node/index.js | 3 +-
.../public/views/elasticsearch/nodes/index.js | 8 +-
.../public/views/kibana/instance/index.js | 24 +-
.../public/views/kibana/instances/index.js | 8 +-
.../views/logstash/node/advanced/index.js | 28 +-
.../public/views/logstash/node/index.js | 24 +-
.../public/views/logstash/nodes/index.js | 8 +-
.../server/alerts/alerts_factory.test.ts | 2 +-
.../server/alerts/alerts_factory.ts | 3 +
.../monitoring/server/alerts/base_alert.ts | 41 +-
.../server/alerts/cpu_usage_alert.ts | 4 +-
.../plugins/monitoring/server/alerts/index.ts | 1 +
.../missing_monitoring_data_alert.test.ts | 459 ++++++++++++++++
.../alerts/missing_monitoring_data_alert.ts | 504 ++++++++++++++++++
.../monitoring/server/alerts/types.d.ts | 16 +
.../server/lib/alerts/fetch_clusters.ts | 19 +-
.../fetch_missing_monitoring_data.test.ts | 249 +++++++++
.../alerts/fetch_missing_monitoring_data.ts | 275 ++++++++++
.../get_listing_link_for_stack_product.ts | 28 +
.../lib/alerts/get_stack_product_label.ts | 17 +
.../get_type_label_for_stack_product.ts | 51 ++
59 files changed, 2303 insertions(+), 89 deletions(-)
create mode 100644 x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts
create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx
create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts
create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx
create mode 100644 x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx
create mode 100644 x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
create mode 100644 x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_listing_link_for_stack_product.ts
create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_stack_product_label.ts
create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/get_type_label_for_stack_product.ts
diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts
index 6eb0d6e93d58a0..860f6439f3fdf5 100644
--- a/x-pack/plugins/monitoring/common/constants.ts
+++ b/x-pack/plugins/monitoring/common/constants.ts
@@ -236,6 +236,7 @@ export const ALERT_NODES_CHANGED = `${ALERT_PREFIX}alert_nodes_changed`;
export const ALERT_ELASTICSEARCH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_elasticsearch_version_mismatch`;
export const ALERT_KIBANA_VERSION_MISMATCH = `${ALERT_PREFIX}alert_kibana_version_mismatch`;
export const ALERT_LOGSTASH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_logstash_version_mismatch`;
+export const ALERT_MISSING_MONITORING_DATA = `${ALERT_PREFIX}alert_missing_monitoring_data`;
/**
* A listing of all alert types
@@ -249,6 +250,7 @@ export const ALERTS = [
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
ALERT_KIBANA_VERSION_MISMATCH,
ALERT_LOGSTASH_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
];
/**
diff --git a/x-pack/plugins/monitoring/common/types.ts b/x-pack/plugins/monitoring/common/types.ts
index 4216a046fef9f3..825d2e454b3bba 100644
--- a/x-pack/plugins/monitoring/common/types.ts
+++ b/x-pack/plugins/monitoring/common/types.ts
@@ -31,10 +31,14 @@ export interface CommonAlertFilter {
nodeUuid?: string;
}
-export interface CommonAlertCpuUsageFilter extends CommonAlertFilter {
+export interface CommonAlertNodeUuidFilter extends CommonAlertFilter {
nodeUuid: string;
}
+export interface CommonAlertStackProductFilter extends CommonAlertFilter {
+ stackProduct: string;
+}
+
export interface CommonAlertParamDetail {
label: string;
type: AlertParamType;
diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx
index 1d67eebb1705cc..cf75939b14efc3 100644
--- a/x-pack/plugins/monitoring/public/alerts/badge.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx
@@ -18,7 +18,7 @@ import { CommonAlertStatus, CommonAlertState } from '../../common/types';
import { AlertSeverity } from '../../common/enums';
// @ts-ignore
import { formatDateTimeLocal } from '../../common/formatting';
-import { AlertState } from '../../server/alerts/types';
+import { AlertMessage, AlertState } from '../../server/alerts/types';
import { AlertPanel } from './panel';
import { Legacy } from '../legacy_shims';
import { isInSetupMode } from '../lib/setup_mode';
@@ -39,9 +39,10 @@ interface AlertInPanel {
interface Props {
alerts: { [alertTypeId: string]: CommonAlertStatus };
stateFilter: (state: AlertState) => boolean;
+ nextStepsFilter: (nextStep: AlertMessage) => boolean;
}
export const AlertsBadge: React.FC = (props: Props) => {
- const { stateFilter = () => true } = props;
+ const { stateFilter = () => true, nextStepsFilter = () => true } = props;
const [showPopover, setShowPopover] = React.useState(null);
const inSetupMode = isInSetupMode();
const alerts = Object.values(props.alerts).filter(Boolean);
@@ -80,7 +81,7 @@ export const AlertsBadge: React.FC = (props: Props) => {
id: index + 1,
title: alertStatus.alert.label,
width: 400,
- content: ,
+ content: ,
};
}),
];
@@ -158,7 +159,13 @@ export const AlertsBadge: React.FC = (props: Props) => {
id: index + 1,
title: getDateFromState(alertStatus.alertState),
width: 400,
- content: ,
+ content: (
+
+ ),
};
}),
];
diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx
index cad98dd1e6aec4..1ddd41c2684565 100644
--- a/x-pack/plugins/monitoring/public/alerts/callout.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx
@@ -32,9 +32,10 @@ const TYPES = [
interface Props {
alerts: { [alertTypeId: string]: CommonAlertStatus };
stateFilter: (state: AlertState) => boolean;
+ nextStepsFilter: (nextStep: AlertMessage) => boolean;
}
export const AlertsCallout: React.FC = (props: Props) => {
- const { alerts, stateFilter = () => true } = props;
+ const { alerts, stateFilter = () => true, nextStepsFilter = () => true } = props;
const callouts = TYPES.map((type) => {
const list = [];
@@ -56,11 +57,11 @@ export const AlertsCallout: React.FC = (props: Props) => {
const nextStepsUi =
state.ui.message.nextSteps && state.ui.message.nextSteps.length ? (
- {state.ui.message.nextSteps.map(
- (step: AlertMessage, nextStepIndex: number) => (
+ {state.ui.message.nextSteps
+ .filter(nextStepsFilter)
+ .map((step: AlertMessage, nextStepIndex: number) => (
- {replaceTokens(step)}
- )
- )}
+ ))}
) : null;
diff --git a/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts b/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts
new file mode 100644
index 00000000000000..63714a6921e3f1
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { CommonAlertState, CommonAlertStatus } from '../../common/types';
+
+export function filterAlertStates(
+ alerts: { [type: string]: CommonAlertStatus },
+ filter: (type: string, state: CommonAlertState) => boolean
+) {
+ return Object.keys(alerts).reduce(
+ (accum: { [type: string]: CommonAlertStatus }, type: string) => {
+ accum[type] = {
+ ...alerts[type],
+ states: alerts[type].states.filter((state) => filter(type, state)),
+ };
+ return accum;
+ },
+ {}
+ );
+}
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx
new file mode 100644
index 00000000000000..7dc6155de529ee
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/expression.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment } from 'react';
+import { EuiForm, EuiSpacer } from '@elastic/eui';
+import { CommonAlertParamDetails } from '../../../common/types';
+import { AlertParamDuration } from '../flyout_expressions/alert_param_duration';
+import { AlertParamType } from '../../../common/enums';
+import { AlertParamPercentage } from '../flyout_expressions/alert_param_percentage';
+
+export interface Props {
+ alertParams: { [property: string]: any };
+ setAlertParams: (property: string, value: any) => void;
+ setAlertProperty: (property: string, value: any) => void;
+ errors: { [key: string]: string[] };
+ paramDetails: CommonAlertParamDetails;
+}
+
+export const Expression: React.FC = (props) => {
+ const { alertParams, paramDetails, setAlertParams, errors } = props;
+
+ const alertParamsUi = Object.keys(alertParams).map((alertParamName) => {
+ const details = paramDetails[alertParamName];
+ const value = alertParams[alertParamName];
+
+ switch (details.type) {
+ case AlertParamType.Duration:
+ return (
+
+ );
+ case AlertParamType.Percentage:
+ return (
+
+ );
+ }
+ });
+
+ return (
+
+ {alertParamsUi}
+
+
+ );
+};
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts
new file mode 100644
index 00000000000000..5169601c0e6e34
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { createMissingMonitoringDataAlertType } from './missing_monitoring_data_alert';
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx
new file mode 100644
index 00000000000000..bcea98592adb29
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
+import { validate } from './validation';
+import { ALERT_MISSING_MONITORING_DATA } from '../../../common/constants';
+import { Expression } from './expression';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { MissingMonitoringDataAlert } from '../../../server/alerts';
+
+export function createMissingMonitoringDataAlertType(): AlertTypeModel {
+ const alert = new MissingMonitoringDataAlert();
+ return {
+ id: ALERT_MISSING_MONITORING_DATA,
+ name: alert.label,
+ iconClass: 'bell',
+ alertParamsExpression: (props: any) => (
+
+ ),
+ validate,
+ defaultActionMessage: '{{context.internalFullMessage}}',
+ requiresAppContext: true,
+ };
+}
diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/validation.tsx
new file mode 100644
index 00000000000000..fe84de9bd00ea3
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { ValidationResult } from '../../../../triggers_actions_ui/public/types';
+
+export function validate(opts: any): ValidationResult {
+ const validationResult = { errors: {} };
+
+ const errors: { [key: string]: string[] } = {
+ duration: [],
+ limit: [],
+ };
+ if (!opts.duration) {
+ errors.duration.push(
+ i18n.translate('xpack.monitoring.alerts.missingData.validation.duration', {
+ defaultMessage: 'A valid duration is required.',
+ })
+ );
+ }
+ if (!opts.limit) {
+ errors.limit.push(
+ i18n.translate('xpack.monitoring.alerts.missingData.validation.limit', {
+ defaultMessage: 'A valid limit is required.',
+ })
+ );
+ }
+
+ validationResult.errors = errors;
+ return validationResult;
+}
diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx
index 91604acf115fa1..ee605592e94081 100644
--- a/x-pack/plugins/monitoring/public/alerts/panel.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx
@@ -30,11 +30,13 @@ import { BASE_ALERT_API_PATH } from '../../../alerts/common';
interface Props {
alert: CommonAlertStatus;
alertState?: CommonAlertState;
+ nextStepsFilter: (nextStep: AlertMessage) => boolean;
}
export const AlertPanel: React.FC = (props: Props) => {
const {
alert: { alert },
alertState,
+ nextStepsFilter = () => true,
} = props;
const [showFlyout, setShowFlyout] = React.useState(false);
const [isEnabled, setIsEnabled] = React.useState(alert.rawAlert.enabled);
@@ -198,9 +200,11 @@ export const AlertPanel: React.FC = (props: Props) => {
const nextStepsUi =
alertState.state.ui.message.nextSteps && alertState.state.ui.message.nextSteps.length ? (
- {alertState.state.ui.message.nextSteps.map((step: AlertMessage, index: number) => (
-
- ))}
+ {alertState.state.ui.message.nextSteps
+ .filter(nextStepsFilter)
+ .map((step: AlertMessage, index: number) => (
+
+ ))}
) : null;
diff --git a/x-pack/plugins/monitoring/public/alerts/status.tsx b/x-pack/plugins/monitoring/public/alerts/status.tsx
index 0407ddfecf5e90..dba66df0e44741 100644
--- a/x-pack/plugins/monitoring/public/alerts/status.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/status.tsx
@@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { CommonAlertStatus } from '../../common/types';
import { AlertSeverity } from '../../common/enums';
-import { AlertState } from '../../server/alerts/types';
+import { AlertMessage, AlertState } from '../../server/alerts/types';
import { AlertsBadge } from './badge';
import { isInSetupMode } from '../lib/setup_mode';
@@ -18,9 +18,16 @@ interface Props {
showBadge: boolean;
showOnlyCount: boolean;
stateFilter: (state: AlertState) => boolean;
+ nextStepsFilter: (nextStep: AlertMessage) => boolean;
}
export const AlertsStatus: React.FC = (props: Props) => {
- const { alerts, showBadge = false, showOnlyCount = false, stateFilter = () => true } = props;
+ const {
+ alerts,
+ showBadge = false,
+ showOnlyCount = false,
+ stateFilter = () => true,
+ nextStepsFilter = () => true,
+ } = props;
const inSetupMode = isInSetupMode();
if (!alerts) {
@@ -71,7 +78,9 @@ export const AlertsStatus: React.FC = (props: Props) => {
}
if (showBadge || inSetupMode) {
- return ;
+ return (
+
+ );
}
const severity = atLeastOneDanger ? AlertSeverity.Danger : AlertSeverity.Warning;
diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js
index eec24e741ac416..8934bbc41f5f6b 100644
--- a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js
+++ b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js
@@ -18,8 +18,9 @@ import {
} from '@elastic/eui';
import { Status } from './status';
import { FormattedMessage } from '@kbn/i18n/react';
+import { AlertsCallout } from '../../../alerts/callout';
-export function ApmServerInstance({ summary, metrics, ...props }) {
+export function ApmServerInstance({ summary, metrics, alerts, ...props }) {
const seriesToShow = [
metrics.apm_requests,
metrics.apm_responses_valid,
@@ -58,9 +59,18 @@ export function ApmServerInstance({ summary, metrics, ...props }) {
-
+
+ {
+ if (nextStep.text.includes('APM servers')) {
+ return false;
+ }
+ return true;
+ }}
+ />
{charts}
diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/status.js b/x-pack/plugins/monitoring/public/components/apm/instance/status.js
index 9b78db54a2aa75..02a15d214ab9b3 100644
--- a/x-pack/plugins/monitoring/public/components/apm/instance/status.js
+++ b/x-pack/plugins/monitoring/public/components/apm/instance/status.js
@@ -14,7 +14,7 @@ import { CALCULATE_DURATION_SINCE } from '../../../../common/constants';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-export function Status({ stats }) {
+export function Status({ alerts, stats }) {
const { name, output, version, uptime, timeOfLastEvent } = stats;
const metrics = [
@@ -78,6 +78,7 @@ export function Status({ stats }) {
return (
diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js
index e05ba1878caed7..4932fb9068fcc8 100644
--- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js
+++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js
@@ -28,8 +28,9 @@ import { SetupModeBadge } from '../../setup_mode/badge';
import { FormattedMessage } from '@kbn/i18n/react';
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
import { SetupModeFeature } from '../../../../common/enums';
+import { AlertsStatus } from '../../../alerts/status';
-function getColumns(setupMode) {
+function getColumns(alerts, setupMode) {
return [
{
name: i18n.translate('xpack.monitoring.apm.instances.nameTitle', {
@@ -71,6 +72,29 @@ function getColumns(setupMode) {
);
},
},
+ {
+ name: i18n.translate('xpack.monitoring.beats.instances.alertsColumnTitle', {
+ defaultMessage: 'Alerts',
+ }),
+ field: 'alerts',
+ width: '175px',
+ sortable: true,
+ render: (_field, beat) => {
+ return (
+ state.stackProductUuid === beat.uuid}
+ nextStepsFilter={(nextStep) => {
+ if (nextStep.text.includes('APM servers')) {
+ return false;
+ }
+ return true;
+ }}
+ />
+ );
+ },
+ },
{
name: i18n.translate('xpack.monitoring.apm.instances.outputEnabledTitle', {
defaultMessage: 'Output Enabled',
@@ -127,7 +151,7 @@ function getColumns(setupMode) {
];
}
-export function ApmServerInstances({ apms, setupMode }) {
+export function ApmServerInstances({ apms, alerts, setupMode }) {
const { pagination, sorting, onTableChange, data } = apms;
let setupModeCallout = null;
@@ -157,7 +181,7 @@ export function ApmServerInstances({ apms, setupMode }) {
-
+
@@ -165,7 +189,7 @@ export function ApmServerInstances({ apms, setupMode }) {
diff --git a/x-pack/plugins/monitoring/public/components/apm/overview/index.js b/x-pack/plugins/monitoring/public/components/apm/overview/index.js
index 35efa6ac67ea89..b10592c2a49d29 100644
--- a/x-pack/plugins/monitoring/public/components/apm/overview/index.js
+++ b/x-pack/plugins/monitoring/public/components/apm/overview/index.js
@@ -19,7 +19,7 @@ import {
import { Status } from '../instances/status';
import { FormattedMessage } from '@kbn/i18n/react';
-export function ApmOverview({ stats, metrics, ...props }) {
+export function ApmOverview({ stats, metrics, alerts, ...props }) {
const seriesToShow = [
metrics.apm_responses_valid,
metrics.apm_responses_errors,
@@ -54,7 +54,7 @@ export function ApmOverview({ stats, metrics, ...props }) {
-
+
diff --git a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js
index f489271659bfe1..470cdf588ca3d7 100644
--- a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js
+++ b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js
@@ -20,8 +20,9 @@ import {
import { i18n } from '@kbn/i18n';
import { SummaryStatus } from '../../summary_status';
import { FormattedMessage } from '@kbn/i18n/react';
+import { AlertsCallout } from '../../../alerts/callout';
-export function Beat({ summary, metrics, ...props }) {
+export function Beat({ summary, metrics, alerts, ...props }) {
const metricsToShow = [
metrics.beat_event_rates,
metrics.beat_fail_rates,
@@ -134,13 +135,26 @@ export function Beat({ summary, metrics, ...props }) {
-
+
+ {
+ if (nextStep.text.includes('Beat instances')) {
+ return false;
+ }
+ return true;
+ }}
+ />
diff --git a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js
index 60a35e00a4c636..dc65cd38aac533 100644
--- a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js
+++ b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js
@@ -26,10 +26,12 @@ import { SetupModeBadge } from '../../setup_mode/badge';
import { FormattedMessage } from '@kbn/i18n/react';
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
import { SetupModeFeature } from '../../../../common/enums';
+import { AlertsStatus } from '../../../alerts/status';
export class Listing extends PureComponent {
getColumns() {
const setupMode = this.props.setupMode;
+ const alerts = this.props.alerts;
return [
{
@@ -72,6 +74,29 @@ export class Listing extends PureComponent {
);
},
},
+ {
+ name: i18n.translate('xpack.monitoring.beats.instances.alertsColumnTitle', {
+ defaultMessage: 'Alerts',
+ }),
+ field: 'alerts',
+ width: '175px',
+ sortable: true,
+ render: (_field, beat) => {
+ return (
+ state.stackProductUuid === beat.uuid}
+ nextStepsFilter={(nextStep) => {
+ if (nextStep.text.includes('Beat instances')) {
+ return false;
+ }
+ return true;
+ }}
+ />
+ );
+ },
+ },
{
name: i18n.translate('xpack.monitoring.beats.instances.typeTitle', {
defaultMessage: 'Type',
@@ -122,7 +147,7 @@ export class Listing extends PureComponent {
}
render() {
- const { stats, data, sorting, pagination, onTableChange, setupMode } = this.props;
+ const { stats, data, sorting, pagination, onTableChange, setupMode, alerts } = this.props;
let setupModeCallOut = null;
if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) {
@@ -155,7 +180,7 @@ export class Listing extends PureComponent {
-
+
diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js
index 897f017f44f418..aa09da31504d3f 100644
--- a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js
+++ b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js
@@ -84,6 +84,7 @@ export function BeatsOverview({
latestVersions,
stats,
metrics,
+ alerts,
...props
}) {
const seriesToShow = [
@@ -113,7 +114,7 @@ export function BeatsOverview({
-
+
{renderLatestActive(latestActive, latestTypes, latestVersions)}
diff --git a/x-pack/plugins/monitoring/public/components/beats/stats.js b/x-pack/plugins/monitoring/public/components/beats/stats.js
index 89ec10bbaf1bb6..c1f06af14a4532 100644
--- a/x-pack/plugins/monitoring/public/components/beats/stats.js
+++ b/x-pack/plugins/monitoring/public/components/beats/stats.js
@@ -9,7 +9,7 @@ import { formatMetric } from '../../lib/format_number';
import { SummaryStatus } from '../summary_status';
import { i18n } from '@kbn/i18n';
-export function Stats({ stats }) {
+export function Stats({ stats, alerts }) {
const {
total,
types,
@@ -51,5 +51,5 @@ export function Stats({ stats }) {
'data-test-subj': 'bytesSent',
});
- return ;
+ return ;
}
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js
index 4bf07710393ea7..d0d5a36c3829b2 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js
@@ -24,14 +24,22 @@ import {
EuiFlexGroup,
} from '@elastic/eui';
import { formatTimestampToDuration } from '../../../../common';
-import { CALCULATE_DURATION_SINCE, APM_SYSTEM_ID } from '../../../../common/constants';
+import {
+ CALCULATE_DURATION_SINCE,
+ APM_SYSTEM_ID,
+ ALERT_MISSING_MONITORING_DATA,
+} from '../../../../common/constants';
import { SetupModeTooltip } from '../../setup_mode/tooltip';
import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
import { SetupModeFeature } from '../../../../common/enums';
+import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
+import { AlertsBadge } from '../../../alerts/badge';
+
+const SERVERS_PANEL_ALERTS = [ALERT_MISSING_MONITORING_DATA];
export function ApmPanel(props) {
- const { setupMode } = props;
+ const { setupMode, alerts } = props;
const apmsTotal = get(props, 'apms.total') || 0;
// Do not show if we are not in setup mode
if (apmsTotal === 0 && !setupMode.enabled) {
@@ -50,6 +58,16 @@ export function ApmPanel(props) {
/>
) : null;
+ let apmServersAlertStatus = null;
+ if (shouldShowAlertBadge(alerts, SERVERS_PANEL_ALERTS)) {
+ const alertsList = SERVERS_PANEL_ALERTS.map((alertType) => alerts[alertType]);
+ apmServersAlertStatus = (
+
+
+
+ );
+ }
+
return (
- {setupModeMetricbeatMigrationTooltip}
+
+
+ {setupModeMetricbeatMigrationTooltip}
+ {apmServersAlertStatus}
+
+
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js
index 3d9b455787a96d..628f57a0ffde3e 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js
@@ -23,13 +23,17 @@ import { ClusterItemContainer, DisabledIfNoDataAndInSetupModeLink } from './help
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { SetupModeTooltip } from '../../setup_mode/tooltip';
-import { BEATS_SYSTEM_ID } from '../../../../common/constants';
+import { ALERT_MISSING_MONITORING_DATA, BEATS_SYSTEM_ID } from '../../../../common/constants';
import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
import { SetupModeFeature } from '../../../../common/enums';
+import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
+import { AlertsBadge } from '../../../alerts/badge';
+
+const BEATS_PANEL_ALERTS = [ALERT_MISSING_MONITORING_DATA];
export function BeatsPanel(props) {
- const { setupMode } = props;
+ const { setupMode, alerts } = props;
const beatsTotal = get(props, 'beats.total') || 0;
// Do not show if we are not in setup mode
if (beatsTotal === 0 && !setupMode.enabled) {
@@ -47,6 +51,16 @@ export function BeatsPanel(props) {
/>
) : null;
+ let beatsAlertsStatus = null;
+ if (shouldShowAlertBadge(alerts, BEATS_PANEL_ALERTS)) {
+ const alertsList = BEATS_PANEL_ALERTS.map((alertType) => alerts[alertType]);
+ beatsAlertsStatus = (
+
+
+
+ );
+ }
+
const beatTypes = props.beats.types.map((beat, index) => {
return [
- {setupModeMetricbeatMigrationTooltip}
+
+
+ {setupModeMetricbeatMigrationTooltip}
+ {beatsAlertsStatus}
+
+
{beatTypes}
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js
index 61a24f31ca89ae..667f64458b8f97 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js
@@ -43,6 +43,7 @@ import {
ALERT_DISK_USAGE,
ALERT_NODES_CHANGED,
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
} from '../../../../common/constants';
import { AlertsBadge } from '../../../alerts/badge';
import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
@@ -161,6 +162,7 @@ const NODES_PANEL_ALERTS = [
ALERT_DISK_USAGE,
ALERT_NODES_CHANGED,
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
];
export function ElasticsearchPanel(props) {
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js
index 66701c1dfd95a0..aebd1cee5f0be0 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js
@@ -12,7 +12,16 @@ import { BeatsPanel } from './beats_panel';
import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui';
import { ApmPanel } from './apm_panel';
import { FormattedMessage } from '@kbn/i18n/react';
-import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants';
+import {
+ STANDALONE_CLUSTER_CLUSTER_UUID,
+ ALERT_MISSING_MONITORING_DATA,
+ ELASTICSEARCH_SYSTEM_ID,
+ KIBANA_SYSTEM_ID,
+ LOGSTASH_SYSTEM_ID,
+ BEATS_SYSTEM_ID,
+ APM_SYSTEM_ID,
+} from '../../../../common/constants';
+import { filterAlertStates } from '../../../alerts/filter_alert_states';
export function Overview(props) {
const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID;
@@ -37,12 +46,22 @@ export function Overview(props) {
license={props.cluster.license}
setupMode={props.setupMode}
showLicenseExpiration={props.showLicenseExpiration}
- alerts={props.alerts}
+ alerts={filterAlertStates(props.alerts, (type, { state }) => {
+ if (type === ALERT_MISSING_MONITORING_DATA) {
+ return state.stackProduct === ELASTICSEARCH_SYSTEM_ID;
+ }
+ return true;
+ })}
/>
{
+ if (type === ALERT_MISSING_MONITORING_DATA) {
+ return state.stackProduct === KIBANA_SYSTEM_ID;
+ }
+ return true;
+ })}
/>
) : null}
@@ -50,12 +69,35 @@ export function Overview(props) {
{
+ if (type === ALERT_MISSING_MONITORING_DATA) {
+ return state.stackProduct === LOGSTASH_SYSTEM_ID;
+ }
+ return true;
+ })}
/>
-
+ {
+ if (type === ALERT_MISSING_MONITORING_DATA) {
+ return state.stackProduct === BEATS_SYSTEM_ID;
+ }
+ return true;
+ })}
+ />
-
+ {
+ if (type === ALERT_MISSING_MONITORING_DATA) {
+ return state.stackProduct === APM_SYSTEM_ID;
+ }
+ return true;
+ })}
+ />
);
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js
index 7df0a3ca7138e6..1f20684bd97d72 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js
@@ -28,14 +28,18 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { SetupModeTooltip } from '../../setup_mode/tooltip';
-import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants';
+import {
+ KIBANA_SYSTEM_ID,
+ ALERT_KIBANA_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
+} from '../../../../common/constants';
import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
import { AlertsBadge } from '../../../alerts/badge';
import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
import { SetupModeFeature } from '../../../../common/enums';
-const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH];
+const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA];
export function KibanaPanel(props) {
const setupMode = props.setupMode;
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js
index 2c61438dca17c3..7c0e04ab5d6158 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js
@@ -15,6 +15,7 @@ import {
LOGSTASH,
LOGSTASH_SYSTEM_ID,
ALERT_LOGSTASH_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
} from '../../../../common/constants';
import {
@@ -40,7 +41,7 @@ import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badg
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
import { SetupModeFeature } from '../../../../common/enums';
-const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH];
+const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA];
export function LogstashPanel(props) {
const { setupMode } = props;
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js
index ac1a5212a8d269..47e30b71e03d01 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js
@@ -73,11 +73,22 @@ export const Node = ({
state.nodeId === nodeId}
+ alertsStateFilter={(state) =>
+ state.nodeId === nodeId || state.stackProductUuid === nodeId
+ }
/>
- state.nodeId === nodeId} />
+ state.nodeId === nodeId || state.stackProductUuid === nodeId}
+ nextStepsFilter={(nextStep) => {
+ if (nextStep.text.includes('Elasticsearch nodes')) {
+ return false;
+ }
+ return true;
+ }}
+ />
{metricsToShow.map((metric, index) => (
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
index f088f7c0d348af..41d3a579db5a23 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js
@@ -137,7 +137,15 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler
state.nodeId === node.resolver}
+ stateFilter={(state) =>
+ state.nodeId === node.resolver || state.stackProductUuid === node.resolver
+ }
+ nextStepsFilter={(nextStep) => {
+ if (nextStep.text.includes('Elasticsearch nodes')) {
+ return false;
+ }
+ return true;
+ }}
/>
);
},
diff --git a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js
index 10ad1634eda862..8095337dd37967 100644
--- a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js
+++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js
@@ -91,7 +91,18 @@ const getColumns = (setupMode, alerts) => {
width: '175px',
sortable: true,
render: () => {
- return ;
+ return (
+ {
+ if (nextStep.text.includes('Kibana instances')) {
+ return false;
+ }
+ return true;
+ }}
+ />
+ );
},
},
{
diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js
index 4a1137079ebb41..a5db433bbfe0a8 100644
--- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js
+++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js
@@ -84,7 +84,18 @@ export class Listing extends PureComponent {
width: '175px',
sortable: true,
render: () => {
- return ;
+ return (
+ {
+ if (nextStep.text.includes('Logstash nodes')) {
+ return false;
+ }
+ return true;
+ }}
+ />
+ );
},
},
{
diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts
index a9c26ca7103a2c..f4f66185346e80 100644
--- a/x-pack/plugins/monitoring/public/plugin.ts
+++ b/x-pack/plugins/monitoring/public/plugin.ts
@@ -23,6 +23,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { MonitoringStartPluginDependencies, MonitoringConfig } from './types';
import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public';
import { createCpuUsageAlertType } from './alerts/cpu_usage_alert';
+import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert';
import { createLegacyAlertTypes } from './alerts/legacy_alert';
import { createDiskUsageAlertType } from './alerts/disk_usage_alert';
@@ -72,6 +73,7 @@ export class MonitoringPlugin
}
plugins.triggers_actions_ui.alertTypeRegistry.register(createCpuUsageAlertType());
+ plugins.triggers_actions_ui.alertTypeRegistry.register(createMissingMonitoringDataAlertType());
plugins.triggers_actions_ui.alertTypeRegistry.register(createDiskUsageAlertType());
const legacyAlertTypes = createLegacyAlertTypes();
for (const legacyAlertType of legacyAlertTypes) {
diff --git a/x-pack/plugins/monitoring/public/views/apm/instance/index.js b/x-pack/plugins/monitoring/public/views/apm/instance/index.js
index 752128782194e9..396d4651e0c5ed 100644
--- a/x-pack/plugins/monitoring/public/views/apm/instance/index.js
+++ b/x-pack/plugins/monitoring/public/views/apm/instance/index.js
@@ -18,7 +18,11 @@ import { routeInitProvider } from '../../../lib/route_init';
import template from './index.html';
import { MonitoringViewBaseController } from '../../base_controller';
import { ApmServerInstance } from '../../../components/apm/instance';
-import { CODE_PATH_APM } from '../../../../common/constants';
+import {
+ CODE_PATH_APM,
+ ALERT_MISSING_MONITORING_DATA,
+ APM_SYSTEM_ID,
+} from '../../../../common/constants';
uiRoutes.when('/apm/instances/:uuid', {
template,
@@ -50,6 +54,17 @@ uiRoutes.when('/apm/instances/:uuid', {
reactNodeId: 'apmInstanceReact',
$scope,
$injector,
+ alerts: {
+ shouldFetch: true,
+ options: {
+ alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: APM_SYSTEM_ID,
+ },
+ ],
+ },
+ },
});
$scope.$watch(
@@ -69,6 +84,7 @@ uiRoutes.when('/apm/instances/:uuid', {
summary={data.apmSummary || {}}
metrics={data.metrics || {}}
onBrush={this.onBrush}
+ alerts={this.alerts}
zoomInfo={this.zoomInfo}
/>
);
diff --git a/x-pack/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/plugins/monitoring/public/views/apm/instances/index.js
index 1f5b089ea748e4..75f3ded89a595b 100644
--- a/x-pack/plugins/monitoring/public/views/apm/instances/index.js
+++ b/x-pack/plugins/monitoring/public/views/apm/instances/index.js
@@ -13,7 +13,11 @@ import template from './index.html';
import { ApmServerInstances } from '../../../components/apm/instances';
import { MonitoringViewBaseEuiTableController } from '../..';
import { SetupModeRenderer } from '../../../components/renderers';
-import { APM_SYSTEM_ID, CODE_PATH_APM } from '../../../../common/constants';
+import {
+ APM_SYSTEM_ID,
+ CODE_PATH_APM,
+ ALERT_MISSING_MONITORING_DATA,
+} from '../../../../common/constants';
uiRoutes.when('/apm/instances', {
template,
@@ -47,6 +51,17 @@ uiRoutes.when('/apm/instances', {
reactNodeId: 'apmInstancesReact',
$scope,
$injector,
+ alerts: {
+ shouldFetch: true,
+ options: {
+ alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: APM_SYSTEM_ID,
+ },
+ ],
+ },
+ },
});
this.scope = $scope;
@@ -67,6 +82,7 @@ uiRoutes.when('/apm/instances', {
{flyoutComponent}
this.data,
(data) => {
this.renderReact(
-
+
);
}
);
diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/index.js b/x-pack/plugins/monitoring/public/views/beats/beat/index.js
index 6cffae2479128d..3e9e4e4b0373d9 100644
--- a/x-pack/plugins/monitoring/public/views/beats/beat/index.js
+++ b/x-pack/plugins/monitoring/public/views/beats/beat/index.js
@@ -11,7 +11,11 @@ import { routeInitProvider } from '../../../lib/route_init';
import { MonitoringViewBaseController } from '../../';
import { getPageData } from './get_page_data';
import template from './index.html';
-import { CODE_PATH_BEATS } from '../../../../common/constants';
+import {
+ CODE_PATH_BEATS,
+ ALERT_MISSING_MONITORING_DATA,
+ BEATS_SYSTEM_ID,
+} from '../../../../common/constants';
import { Beat } from '../../../components/beats/beat';
uiRoutes.when('/beats/beat/:beatUuid', {
@@ -52,6 +56,17 @@ uiRoutes.when('/beats/beat/:beatUuid', {
$scope,
$injector,
reactNodeId: 'monitoringBeatsInstanceApp',
+ alerts: {
+ shouldFetch: true,
+ options: {
+ alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: BEATS_SYSTEM_ID,
+ },
+ ],
+ },
+ },
});
this.data = pageData;
@@ -60,6 +75,7 @@ uiRoutes.when('/beats/beat/:beatUuid', {
(data) => {
this.renderReact(
this.data,
(data) => {
this.renderReact(
-
+
);
}
);
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
index 8c30e4a2c1b07e..ff7f29c58b2f6c 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
@@ -20,6 +20,7 @@ import { MonitoringViewBaseController } from '../../../base_controller';
import {
CODE_PATH_ELASTICSEARCH,
ALERT_CPU_USAGE,
+ ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
} from '../../../../../common/constants';
@@ -71,7 +72,7 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', {
alerts: {
shouldFetch: true,
options: {
- alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE],
+ alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA],
filters: [
{
nodeUuid: nodeName,
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
index ed2603e6dfff3c..15b9b7b4c0e4a4 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
@@ -21,6 +21,7 @@ import { MonitoringViewBaseController } from '../../base_controller';
import {
CODE_PATH_ELASTICSEARCH,
ALERT_CPU_USAGE,
+ ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
} from '../../../../common/constants';
@@ -55,7 +56,7 @@ uiRoutes.when('/elasticsearch/nodes/:node', {
alerts: {
shouldFetch: true,
options: {
- alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE],
+ alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA],
filters: [
{
nodeUuid: nodeName,
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
index 66fcac43e4fc5c..ef807bf9b377d8 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
@@ -19,6 +19,7 @@ import {
ELASTICSEARCH_SYSTEM_ID,
CODE_PATH_ELASTICSEARCH,
ALERT_CPU_USAGE,
+ ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
} from '../../../../common/constants';
@@ -87,7 +88,12 @@ uiRoutes.when('/elasticsearch/nodes', {
alerts: {
shouldFetch: true,
options: {
- alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE],
+ alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: ELASTICSEARCH_SYSTEM_ID,
+ },
+ ],
},
},
});
diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js
index 20a1a517194156..29852501d1667b 100644
--- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js
+++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js
@@ -27,7 +27,12 @@ import {
import { MonitoringTimeseriesContainer } from '../../../components/chart';
import { DetailStatus } from '../../../components/kibana/detail_status';
import { MonitoringViewBaseController } from '../../base_controller';
-import { CODE_PATH_KIBANA, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants';
+import {
+ CODE_PATH_KIBANA,
+ ALERT_KIBANA_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
+ KIBANA_SYSTEM_ID,
+} from '../../../../common/constants';
import { AlertsCallout } from '../../../alerts/callout';
function getPageData($injector) {
@@ -76,7 +81,12 @@ uiRoutes.when('/kibana/instances/:uuid', {
alerts: {
shouldFetch: true,
options: {
- alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH],
+ alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: KIBANA_SYSTEM_ID,
+ },
+ ],
},
},
});
@@ -104,7 +114,15 @@ uiRoutes.when('/kibana/instances/:uuid', {
-
+ {
+ if (nextStep.text.includes('Kibana instances')) {
+ return false;
+ }
+ return true;
+ }}
+ />
diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js
index 765e112a23305a..fcb2ee53471a14 100644
--- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js
+++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js
@@ -17,6 +17,7 @@ import {
KIBANA_SYSTEM_ID,
CODE_PATH_KIBANA,
ALERT_KIBANA_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
} from '../../../../common/constants';
uiRoutes.when('/kibana/instances', {
@@ -46,7 +47,12 @@ uiRoutes.when('/kibana/instances', {
alerts: {
shouldFetch: true,
options: {
- alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH],
+ alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: KIBANA_SYSTEM_ID,
+ },
+ ],
},
},
});
diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js
index 0fc3cc47502cd6..591db66b2698c1 100644
--- a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js
+++ b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js
@@ -26,7 +26,13 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { MonitoringTimeseriesContainer } from '../../../../components/chart';
-import { CODE_PATH_LOGSTASH } from '../../../../../common/constants';
+import {
+ CODE_PATH_LOGSTASH,
+ ALERT_LOGSTASH_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
+ LOGSTASH_SYSTEM_ID,
+} from '../../../../../common/constants';
+import { AlertsCallout } from '../../../../alerts/callout';
function getPageData($injector) {
const $http = $injector.get('$http');
@@ -69,6 +75,17 @@ uiRoutes.when('/logstash/node/:uuid/advanced', {
reactNodeId: 'monitoringLogstashNodeAdvancedApp',
$scope,
$injector,
+ alerts: {
+ shouldFetch: true,
+ options: {
+ alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: LOGSTASH_SYSTEM_ID,
+ },
+ ],
+ },
+ },
telemetryPageViewTitle: 'logstash_node_advanced',
});
@@ -112,6 +129,15 @@ uiRoutes.when('/logstash/node/:uuid/advanced', {
+ {
+ if (nextStep.text.includes('Logstash nodes')) {
+ return false;
+ }
+ return true;
+ }}
+ />
{metricsToShow.map((metric, index) => (
diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/index.js
index e2dee77133c72b..cccae6913052ab 100644
--- a/x-pack/plugins/monitoring/public/views/logstash/node/index.js
+++ b/x-pack/plugins/monitoring/public/views/logstash/node/index.js
@@ -26,7 +26,12 @@ import {
} from '@elastic/eui';
import { MonitoringTimeseriesContainer } from '../../../components/chart';
import { MonitoringViewBaseController } from '../../base_controller';
-import { CODE_PATH_LOGSTASH, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants';
+import {
+ CODE_PATH_LOGSTASH,
+ ALERT_LOGSTASH_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
+ LOGSTASH_SYSTEM_ID,
+} from '../../../../common/constants';
import { AlertsCallout } from '../../../alerts/callout';
function getPageData($injector) {
@@ -73,7 +78,12 @@ uiRoutes.when('/logstash/node/:uuid', {
alerts: {
shouldFetch: true,
options: {
- alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH],
+ alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: LOGSTASH_SYSTEM_ID,
+ },
+ ],
},
},
telemetryPageViewTitle: 'logstash_node',
@@ -120,7 +130,15 @@ uiRoutes.when('/logstash/node/:uuid', {
-
+ {
+ if (nextStep.text.includes('Logstash nodes')) {
+ return false;
+ }
+ return true;
+ }}
+ />
{metricsToShow.map((metric, index) => (
diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js
index 6f2e47eb3f918e..20b2f68e2c67e2 100644
--- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js
+++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js
@@ -16,6 +16,7 @@ import {
CODE_PATH_LOGSTASH,
LOGSTASH_SYSTEM_ID,
ALERT_LOGSTASH_VERSION_MISMATCH,
+ ALERT_MISSING_MONITORING_DATA,
} from '../../../../common/constants';
uiRoutes.when('/logstash/nodes', {
@@ -45,7 +46,12 @@ uiRoutes.when('/logstash/nodes', {
alerts: {
shouldFetch: true,
options: {
- alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH],
+ alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
+ filters: [
+ {
+ stackProduct: LOGSTASH_SYSTEM_ID,
+ },
+ ],
},
},
});
diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts
index 60693eb42a30e9..ddc8dcafebd213 100644
--- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts
+++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts
@@ -63,6 +63,6 @@ describe('AlertsFactory', () => {
it('should get all', () => {
const alerts = AlertsFactory.getAll();
- expect(alerts.length).toBe(8);
+ expect(alerts.length).toBe(9);
});
});
diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
index 6b1c0d5fffe186..05a92cea5469b6 100644
--- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
+++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts
@@ -6,6 +6,7 @@
import {
CpuUsageAlert,
+ MissingMonitoringDataAlert,
DiskUsageAlert,
NodesChangedAlert,
ClusterHealthAlert,
@@ -19,6 +20,7 @@ import {
ALERT_CLUSTER_HEALTH,
ALERT_LICENSE_EXPIRATION,
ALERT_CPU_USAGE,
+ ALERT_MISSING_MONITORING_DATA,
ALERT_DISK_USAGE,
ALERT_NODES_CHANGED,
ALERT_LOGSTASH_VERSION_MISMATCH,
@@ -31,6 +33,7 @@ export const BY_TYPE = {
[ALERT_CLUSTER_HEALTH]: ClusterHealthAlert,
[ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert,
[ALERT_CPU_USAGE]: CpuUsageAlert,
+ [ALERT_MISSING_MONITORING_DATA]: MissingMonitoringDataAlert,
[ALERT_DISK_USAGE]: DiskUsageAlert,
[ALERT_NODES_CHANGED]: NodesChangedAlert,
[ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert,
diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
index aff84710d27add..61486626040f73 100644
--- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
@@ -198,6 +198,15 @@ export class BaseAlert {
const alertInstance: RawAlertInstance = states.alertInstances[instanceId];
if (alertInstance && this.filterAlertInstance(alertInstance, filters)) {
accum[instanceId] = alertInstance;
+ if (alertInstance.state) {
+ accum[instanceId].state = {
+ alertStates: (alertInstance.state as AlertInstanceState).alertStates.filter(
+ (alertState: AlertState) => {
+ return this.filterAlertState(alertState, filters);
+ }
+ ),
+ };
+ }
}
return accum;
},
@@ -209,6 +218,10 @@ export class BaseAlert {
return true;
}
+ protected filterAlertState(alertState: AlertState, filters: CommonAlertFilter[]) {
+ return true;
+ }
+
protected async execute({ services, params, state }: AlertExecutorOptions): Promise {
const logger = this.getLogger(this.type);
logger.debug(
@@ -226,13 +239,7 @@ export class BaseAlert {
return await mbSafeQuery(async () => _callCluster(endpoint, clientParams, options));
};
const availableCcs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : [];
- // Support CCS use cases by querying to find available remote clusters
- // and then adding those to the index pattern we are searching against
- let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH);
- if (availableCcs) {
- esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs);
- }
- const clusters = await fetchClusters(callCluster, esIndexPattern);
+ const clusters = await this.fetchClusters(callCluster, availableCcs, params);
const uiSettings = (await this.getUiSettingsService()).asScopedToClient(
services.savedObjectsClient
);
@@ -241,6 +248,26 @@ export class BaseAlert {
return await this.processData(data, clusters, services, logger, state);
}
+ protected async fetchClusters(
+ callCluster: any,
+ availableCcs: string[] | undefined = undefined,
+ params: CommonAlertParams
+ ) {
+ let ccs;
+ if (!availableCcs) {
+ ccs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : undefined;
+ } else {
+ ccs = availableCcs;
+ }
+ // Support CCS use cases by querying to find available remote clusters
+ // and then adding those to the index pattern we are searching against
+ let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH);
+ if (ccs) {
+ esIndexPattern = getCcsIndexPattern(esIndexPattern, ccs);
+ }
+ return await fetchClusters(callCluster, esIndexPattern);
+ }
+
protected async fetchData(
params: CommonAlertParams,
callCluster: any,
diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
index 4228354f52748e..ca9674c57216b7 100644
--- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts
@@ -26,7 +26,7 @@ import { RawAlertInstance } from '../../../alerts/common';
import { parseDuration } from '../../../alerts/common/parse_duration';
import {
CommonAlertFilter,
- CommonAlertCpuUsageFilter,
+ CommonAlertNodeUuidFilter,
CommonAlertParams,
CommonAlertParamDetail,
} from '../../common/types';
@@ -129,7 +129,7 @@ export class CpuUsageAlert extends BaseAlert {
const alertInstanceState = (alertInstance.state as unknown) as AlertInstanceState;
if (filters && filters.length) {
for (const _filter of filters) {
- const filter = _filter as CommonAlertCpuUsageFilter;
+ const filter = _filter as CommonAlertNodeUuidFilter;
if (filter && filter.nodeUuid) {
let nodeExistsInStates = false;
for (const state of alertInstanceState.alertStates) {
diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts
index 8fdac655144776..41f6daa38d1dca 100644
--- a/x-pack/plugins/monitoring/server/alerts/index.ts
+++ b/x-pack/plugins/monitoring/server/alerts/index.ts
@@ -6,6 +6,7 @@
export { BaseAlert } from './base_alert';
export { CpuUsageAlert } from './cpu_usage_alert';
+export { MissingMonitoringDataAlert } from './missing_monitoring_data_alert';
export { DiskUsageAlert } from './disk_usage_alert';
export { ClusterHealthAlert } from './cluster_health_alert';
export { LicenseExpirationAlert } from './license_expiration_alert';
diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
new file mode 100644
index 00000000000000..4c06d9718c4559
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts
@@ -0,0 +1,459 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { MissingMonitoringDataAlert } from './missing_monitoring_data_alert';
+import { ALERT_MISSING_MONITORING_DATA } from '../../common/constants';
+import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data';
+import { fetchClusters } from '../lib/alerts/fetch_clusters';
+
+const RealDate = Date;
+
+jest.mock('../lib/alerts/fetch_missing_monitoring_data', () => ({
+ fetchMissingMonitoringData: jest.fn(),
+}));
+jest.mock('../lib/alerts/fetch_clusters', () => ({
+ fetchClusters: jest.fn(),
+}));
+
+describe('MissingMonitoringDataAlert', () => {
+ it('should have defaults', () => {
+ const alert = new MissingMonitoringDataAlert();
+ expect(alert.type).toBe(ALERT_MISSING_MONITORING_DATA);
+ expect(alert.label).toBe('Missing monitoring data');
+ expect(alert.defaultThrottle).toBe('1d');
+ // @ts-ignore
+ expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '5m' });
+ // @ts-ignore
+ expect(alert.actionVariables).toStrictEqual([
+ { name: 'stackProducts', description: 'The stack products missing monitoring data.' },
+ { name: 'count', description: 'The number of stack products missing monitoring data.' },
+ {
+ name: 'internalShortMessage',
+ description: 'The short internal message generated by Elastic.',
+ },
+ {
+ name: 'internalFullMessage',
+ description: 'The full internal message generated by Elastic.',
+ },
+ { name: 'state', description: 'The current state of the alert.' },
+ { name: 'clusterName', description: 'The cluster to which the nodes belong.' },
+ { name: 'action', description: 'The recommended action for this alert.' },
+ {
+ name: 'actionPlain',
+ description: 'The recommended action for this alert, without any markdown.',
+ },
+ ]);
+ });
+
+ describe('execute', () => {
+ function FakeDate() {}
+ FakeDate.prototype.valueOf = () => 1;
+
+ const clusterUuid = 'abc123';
+ const clusterName = 'testCluster';
+ const stackProduct = 'elasticsearch';
+ const stackProductUuid = 'esNode1';
+ const stackProductName = 'esName1';
+ const gapDuration = 3000001;
+ const missingData = [
+ {
+ stackProduct,
+ stackProductUuid,
+ stackProductName,
+ clusterUuid,
+ gapDuration,
+ },
+ {
+ stackProduct: 'kibana',
+ stackProductUuid: 'kibanaUuid1',
+ stackProductName: 'kibanaInstance1',
+ clusterUuid,
+ gapDuration: gapDuration + 10,
+ },
+ ];
+ const getUiSettingsService = () => ({
+ asScopedToClient: jest.fn(),
+ });
+ const getLogger = () => ({
+ debug: jest.fn(),
+ });
+ const monitoringCluster = null;
+ const config = {
+ ui: {
+ ccs: { enabled: true },
+ container: { elasticsearch: { enabled: false } },
+ metricbeat: { index: 'metricbeat-*' },
+ },
+ };
+ const kibanaUrl = 'http://localhost:5601';
+
+ const replaceState = jest.fn();
+ const scheduleActions = jest.fn();
+ const getState = jest.fn();
+ const executorOptions = {
+ services: {
+ callCluster: jest.fn(),
+ alertInstanceFactory: jest.fn().mockImplementation(() => {
+ return {
+ replaceState,
+ scheduleActions,
+ getState,
+ };
+ }),
+ },
+ state: {},
+ };
+
+ beforeEach(() => {
+ // @ts-ignore
+ Date = FakeDate;
+ (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
+ return missingData;
+ });
+ (fetchClusters as jest.Mock).mockImplementation(() => {
+ return [{ clusterUuid, clusterName }];
+ });
+ });
+
+ afterEach(() => {
+ Date = RealDate;
+ replaceState.mockReset();
+ scheduleActions.mockReset();
+ getState.mockReset();
+ });
+
+ it('should fire actions', async () => {
+ const alert = new MissingMonitoringDataAlert();
+ alert.initializeAlertType(
+ getUiSettingsService as any,
+ monitoringCluster as any,
+ getLogger as any,
+ config as any,
+ kibanaUrl,
+ false
+ );
+ const type = alert.getAlertType();
+ await type.executor({
+ ...executorOptions,
+ // @ts-ignore
+ params: alert.defaultParams,
+ } as any);
+ const count = 2;
+ expect(replaceState).toHaveBeenCalledWith({
+ alertStates: [
+ {
+ ccs: undefined,
+ cluster: { clusterUuid, clusterName },
+ gapDuration,
+ stackProduct,
+ stackProductName,
+ stackProductUuid,
+ ui: {
+ isFiring: true,
+ message: {
+ text:
+ 'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute',
+ nextSteps: [
+ {
+ text: '#start_linkView all Elasticsearch nodes#end_link',
+ tokens: [
+ {
+ startToken: '#start_link',
+ endToken: '#end_link',
+ type: 'link',
+ url: 'elasticsearch/nodes',
+ },
+ ],
+ },
+ {
+ text: 'Verify monitoring settings on the node',
+ },
+ ],
+ tokens: [
+ {
+ startToken: '#absolute',
+ type: 'time',
+ isAbsolute: true,
+ isRelative: false,
+ timestamp: 1,
+ },
+ ],
+ },
+ severity: 'danger',
+ resolvedMS: 0,
+ triggeredMS: 1,
+ lastCheckedMS: 0,
+ },
+ },
+ {
+ ccs: undefined,
+ cluster: { clusterUuid, clusterName },
+ gapDuration: gapDuration + 10,
+ stackProduct: 'kibana',
+ stackProductName: 'kibanaInstance1',
+ stackProductUuid: 'kibanaUuid1',
+ ui: {
+ isFiring: true,
+ message: {
+ text:
+ 'For the past an hour, we have not detected any monitoring data from the Kibana instance: kibanaInstance1, starting at #absolute',
+ nextSteps: [
+ {
+ text: '#start_linkView all Kibana instances#end_link',
+ tokens: [
+ {
+ startToken: '#start_link',
+ endToken: '#end_link',
+ type: 'link',
+ url: 'kibana/instances',
+ },
+ ],
+ },
+ {
+ text: 'Verify monitoring settings on the instance',
+ },
+ ],
+ tokens: [
+ {
+ startToken: '#absolute',
+ type: 'time',
+ isAbsolute: true,
+ isRelative: false,
+ timestamp: 1,
+ },
+ ],
+ },
+ severity: 'danger',
+ resolvedMS: 0,
+ triggeredMS: 1,
+ lastCheckedMS: 0,
+ },
+ },
+ ],
+ });
+ expect(scheduleActions).toHaveBeenCalledWith('default', {
+ internalFullMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. [View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`,
+ internalShortMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`,
+ action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`,
+ actionPlain:
+ 'Verify these stack products are up and running, then double check the monitoring settings.',
+ clusterName,
+ count,
+ stackProducts: 'Elasticsearch node: esName1, Kibana instance: kibanaInstance1',
+ state: 'firing',
+ });
+ });
+
+ it('should not fire actions if under threshold', async () => {
+ (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
+ return [
+ {
+ ...missingData[0],
+ gapDuration: 1,
+ },
+ ];
+ });
+ const alert = new MissingMonitoringDataAlert();
+ alert.initializeAlertType(
+ getUiSettingsService as any,
+ monitoringCluster as any,
+ getLogger as any,
+ config as any,
+ kibanaUrl,
+ false
+ );
+ const type = alert.getAlertType();
+ await type.executor({
+ ...executorOptions,
+ // @ts-ignore
+ params: alert.defaultParams,
+ } as any);
+ expect(replaceState).toHaveBeenCalledWith({
+ alertStates: [
+ {
+ cluster: {
+ clusterUuid,
+ clusterName,
+ },
+ gapDuration: 1,
+ stackProduct,
+ stackProductName,
+ stackProductUuid,
+ ui: {
+ isFiring: false,
+ lastCheckedMS: 0,
+ message: null,
+ resolvedMS: 0,
+ severity: 'danger',
+ triggeredMS: 0,
+ },
+ },
+ ],
+ });
+ expect(scheduleActions).not.toHaveBeenCalled();
+ });
+
+ it('should resolve with a resolved message', async () => {
+ (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
+ return [
+ {
+ ...missingData[0],
+ gapDuration: 1,
+ },
+ ];
+ });
+ (getState as jest.Mock).mockImplementation(() => {
+ return {
+ alertStates: [
+ {
+ cluster: {
+ clusterUuid,
+ clusterName,
+ },
+ ccs: null,
+ gapDuration: 1,
+ stackProduct,
+ stackProductName,
+ stackProductUuid,
+ ui: {
+ isFiring: true,
+ message: null,
+ severity: 'danger',
+ resolvedMS: 0,
+ triggeredMS: 1,
+ lastCheckedMS: 0,
+ },
+ },
+ ],
+ };
+ });
+ const alert = new MissingMonitoringDataAlert();
+ alert.initializeAlertType(
+ getUiSettingsService as any,
+ monitoringCluster as any,
+ getLogger as any,
+ config as any,
+ kibanaUrl,
+ false
+ );
+ const type = alert.getAlertType();
+ await type.executor({
+ ...executorOptions,
+ // @ts-ignore
+ params: alert.defaultParams,
+ } as any);
+ const count = 1;
+ expect(replaceState).toHaveBeenCalledWith({
+ alertStates: [
+ {
+ cluster: { clusterUuid, clusterName },
+ ccs: null,
+ gapDuration: 1,
+ stackProduct,
+ stackProductName,
+ stackProductUuid,
+ ui: {
+ isFiring: false,
+ message: {
+ text:
+ 'We are now seeing monitoring data for the Elasticsearch node: esName1, as of #resolved',
+ tokens: [
+ {
+ startToken: '#resolved',
+ type: 'time',
+ isAbsolute: true,
+ isRelative: false,
+ timestamp: 1,
+ },
+ ],
+ },
+ severity: 'danger',
+ resolvedMS: 1,
+ triggeredMS: 1,
+ lastCheckedMS: 0,
+ },
+ },
+ ],
+ });
+ expect(scheduleActions).toHaveBeenCalledWith('default', {
+ internalFullMessage: `We are now seeing monitoring data for 1 stack product(s) in cluster testCluster.`,
+ internalShortMessage: `We are now seeing monitoring data for 1 stack product(s) in cluster: testCluster.`,
+ clusterName,
+ count,
+ stackProducts: 'Elasticsearch node: esName1',
+ state: 'resolved',
+ });
+ });
+
+ it('should handle ccs', async () => {
+ const ccs = 'testCluster';
+ (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
+ return [
+ {
+ ...missingData[0],
+ ccs,
+ },
+ ];
+ });
+ const alert = new MissingMonitoringDataAlert();
+ alert.initializeAlertType(
+ getUiSettingsService as any,
+ monitoringCluster as any,
+ getLogger as any,
+ config as any,
+ kibanaUrl,
+ false
+ );
+ const type = alert.getAlertType();
+ await type.executor({
+ ...executorOptions,
+ // @ts-ignore
+ params: alert.defaultParams,
+ } as any);
+ const count = 1;
+ expect(scheduleActions).toHaveBeenCalledWith('default', {
+ internalFullMessage: `We have not detected any monitoring data for 1 stack product(s) in cluster: testCluster. [View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123,ccs:testCluster))`,
+ internalShortMessage: `We have not detected any monitoring data for 1 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`,
+ action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123,ccs:testCluster))`,
+ actionPlain:
+ 'Verify these stack products are up and running, then double check the monitoring settings.',
+ clusterName,
+ count,
+ stackProducts: 'Elasticsearch node: esName1',
+ state: 'firing',
+ });
+ });
+
+ it('should fire with different messaging for cloud', async () => {
+ const alert = new MissingMonitoringDataAlert();
+ alert.initializeAlertType(
+ getUiSettingsService as any,
+ monitoringCluster as any,
+ getLogger as any,
+ config as any,
+ kibanaUrl,
+ true
+ );
+ const type = alert.getAlertType();
+ await type.executor({
+ ...executorOptions,
+ // @ts-ignore
+ params: alert.defaultParams,
+ } as any);
+ const count = 2;
+ expect(scheduleActions).toHaveBeenCalledWith('default', {
+ internalFullMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`,
+ internalShortMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`,
+ action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`,
+ actionPlain:
+ 'Verify these stack products are up and running, then double check the monitoring settings.',
+ clusterName,
+ count,
+ stackProducts: 'Elasticsearch node: esName1, Kibana instance: kibanaInstance1',
+ state: 'firing',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
new file mode 100644
index 00000000000000..6017314f332e68
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts
@@ -0,0 +1,504 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { IUiSettingsClient, Logger } from 'kibana/server';
+import { i18n } from '@kbn/i18n';
+import moment from 'moment';
+import { BaseAlert } from './base_alert';
+import {
+ AlertData,
+ AlertCluster,
+ AlertState,
+ AlertMessage,
+ AlertMissingDataState,
+ AlertMissingData,
+ AlertMessageTimeToken,
+ AlertInstanceState,
+} from './types';
+import { AlertInstance, AlertServices } from '../../../alerts/server';
+import {
+ INDEX_PATTERN,
+ ALERT_MISSING_MONITORING_DATA,
+ INDEX_PATTERN_ELASTICSEARCH,
+} from '../../common/constants';
+import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
+import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums';
+import { RawAlertInstance } from '../../../alerts/common';
+import { parseDuration } from '../../../alerts/common/parse_duration';
+import {
+ CommonAlertFilter,
+ CommonAlertParams,
+ CommonAlertParamDetail,
+ CommonAlertStackProductFilter,
+ CommonAlertNodeUuidFilter,
+} from '../../common/types';
+import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
+import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data';
+import { getTypeLabelForStackProduct } from '../lib/alerts/get_type_label_for_stack_product';
+import { getListingLinkForStackProduct } from '../lib/alerts/get_listing_link_for_stack_product';
+import { getStackProductLabel } from '../lib/alerts/get_stack_product_label';
+import { fetchClusters } from '../lib/alerts/fetch_clusters';
+import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs';
+import { AlertingDefaults, createLink } from './alerts_common';
+
+const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', {
+ defaultMessage: 'resolved',
+});
+const FIRING = i18n.translate('xpack.monitoring.alerts.missingData.firing', {
+ defaultMessage: 'firing',
+});
+
+const DEFAULT_DURATION = '5m';
+const DEFAULT_LIMIT = '1d';
+
+// Go a bit farther back because we need to detect the difference between seeing the monitoring data versus just not looking far enough back
+const LIMIT_BUFFER = 3 * 60 * 1000;
+
+interface MissingDataParams {
+ duration: string;
+ limit: string;
+}
+
+export class MissingMonitoringDataAlert extends BaseAlert {
+ public static paramDetails = {
+ duration: {
+ label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', {
+ defaultMessage: `Notify if monitoring data is missing for`,
+ }),
+ type: AlertParamType.Duration,
+ } as CommonAlertParamDetail,
+ limit: {
+ label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.limit.label', {
+ defaultMessage: `Look this far back in time for monitoring data`,
+ }),
+ type: AlertParamType.Duration,
+ } as CommonAlertParamDetail,
+ };
+
+ public type = ALERT_MISSING_MONITORING_DATA;
+ public label = i18n.translate('xpack.monitoring.alerts.missingData.label', {
+ defaultMessage: 'Missing monitoring data',
+ });
+
+ protected defaultParams: MissingDataParams = {
+ duration: DEFAULT_DURATION,
+ limit: DEFAULT_LIMIT,
+ };
+
+ protected actionVariables = [
+ {
+ name: 'stackProducts',
+ description: i18n.translate(
+ 'xpack.monitoring.alerts.missingData.actionVariables.stackProducts',
+ {
+ defaultMessage: 'The stack products missing monitoring data.',
+ }
+ ),
+ },
+ {
+ name: 'count',
+ description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.count', {
+ defaultMessage: 'The number of stack products missing monitoring data.',
+ }),
+ },
+ ...Object.values(AlertingDefaults.ALERT_TYPE.context),
+ ];
+
+ protected async fetchClusters(
+ callCluster: any,
+ availableCcs: string[] | undefined = undefined,
+ params: CommonAlertParams
+ ) {
+ const limit = parseDuration(((params as unknown) as MissingDataParams).limit);
+ let ccs;
+ if (!availableCcs) {
+ ccs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : undefined;
+ } else {
+ ccs = availableCcs;
+ }
+ // Support CCS use cases by querying to find available remote clusters
+ // and then adding those to the index pattern we are searching against
+ let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH);
+ if (ccs) {
+ esIndexPattern = getCcsIndexPattern(esIndexPattern, ccs);
+ }
+ return await fetchClusters(callCluster, esIndexPattern, {
+ timestamp: {
+ format: 'epoch_millis',
+ gte: limit - LIMIT_BUFFER,
+ },
+ });
+ }
+
+ protected async fetchData(
+ params: CommonAlertParams,
+ callCluster: any,
+ clusters: AlertCluster[],
+ uiSettings: IUiSettingsClient,
+ availableCcs: string[]
+ ): Promise {
+ let indexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN);
+ if (availableCcs) {
+ indexPattern = getCcsIndexPattern(indexPattern, availableCcs);
+ }
+ const duration = parseDuration(((params as unknown) as MissingDataParams).duration);
+ const limit = parseDuration(((params as unknown) as MissingDataParams).limit);
+ const now = +new Date();
+ const missingData = await fetchMissingMonitoringData(
+ callCluster,
+ clusters,
+ indexPattern,
+ this.config.ui.max_bucket_size,
+ now,
+ now - limit - LIMIT_BUFFER
+ );
+ return missingData.map((missing) => {
+ return {
+ instanceKey: `${missing.clusterUuid}:${missing.stackProduct}:${missing.stackProductUuid}`,
+ clusterUuid: missing.clusterUuid,
+ shouldFire: missing.gapDuration > duration,
+ severity: AlertSeverity.Danger,
+ meta: { missing, limit },
+ ccs: missing.ccs,
+ };
+ });
+ }
+
+ protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) {
+ const alertInstanceState = (alertInstance.state as unknown) as AlertInstanceState;
+ if (filters && filters.length) {
+ for (const filter of filters) {
+ const stackProductFilter = filter as CommonAlertStackProductFilter;
+ if (stackProductFilter && stackProductFilter.stackProduct) {
+ let existsInState = false;
+ for (const state of alertInstanceState.alertStates) {
+ if ((state as AlertMissingDataState).stackProduct === stackProductFilter.stackProduct) {
+ existsInState = true;
+ break;
+ }
+ }
+ if (!existsInState) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ protected filterAlertState(alertState: AlertState, filters: CommonAlertFilter[]) {
+ const state = alertState as AlertMissingDataState;
+ if (filters && filters.length) {
+ for (const filter of filters) {
+ const stackProductFilter = filter as CommonAlertStackProductFilter;
+ if (stackProductFilter && stackProductFilter.stackProduct) {
+ if (state.stackProduct !== stackProductFilter.stackProduct) {
+ return false;
+ }
+ }
+
+ const nodeUuidFilter = filter as CommonAlertNodeUuidFilter;
+ if (nodeUuidFilter && nodeUuidFilter.nodeUuid) {
+ if (state.stackProductUuid !== nodeUuidFilter.nodeUuid) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState {
+ const base = super.getDefaultAlertState(cluster, item);
+ return {
+ ...base,
+ ui: {
+ ...base.ui,
+ severity: AlertSeverity.Danger,
+ },
+ };
+ }
+
+ protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage {
+ const { missing, limit } = item.meta as { missing: AlertMissingData; limit: number };
+ if (!alertState.ui.isFiring) {
+ if (missing.gapDuration > limit) {
+ return {
+ text: i18n.translate('xpack.monitoring.alerts.missingData.ui.notQuiteResolvedMessage', {
+ defaultMessage: `We are still not seeing monitoring data for the {stackProduct} {type}: {stackProductName} and will stop trying. To change this, configure the alert to look farther back for data.`,
+ values: {
+ stackProduct: getStackProductLabel(missing.stackProduct),
+ type: getTypeLabelForStackProduct(missing.stackProduct, false),
+ stackProductName: missing.stackProductName,
+ },
+ }),
+ };
+ }
+ return {
+ text: i18n.translate('xpack.monitoring.alerts.missingData.ui.resolvedMessage', {
+ defaultMessage: `We are now seeing monitoring data for the {stackProduct} {type}: {stackProductName}, as of #resolved`,
+ values: {
+ stackProduct: getStackProductLabel(missing.stackProduct),
+ type: getTypeLabelForStackProduct(missing.stackProduct, false),
+ stackProductName: missing.stackProductName,
+ },
+ }),
+ tokens: [
+ {
+ startToken: '#resolved',
+ type: AlertMessageTokenType.Time,
+ isAbsolute: true,
+ isRelative: false,
+ timestamp: alertState.ui.resolvedMS,
+ } as AlertMessageTimeToken,
+ ],
+ };
+ }
+ return {
+ text: i18n.translate('xpack.monitoring.alerts.missingData.ui.firingMessage', {
+ defaultMessage: `For the past {gapDuration}, we have not detected any monitoring data from the {stackProduct} {type}: {stackProductName}, starting at #absolute`,
+ values: {
+ gapDuration: moment.duration(missing.gapDuration, 'milliseconds').humanize(),
+ stackProduct: getStackProductLabel(missing.stackProduct),
+ type: getTypeLabelForStackProduct(missing.stackProduct, false),
+ stackProductName: missing.stackProductName,
+ },
+ }),
+ nextSteps: [
+ createLink(
+ i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.viewAll', {
+ defaultMessage: `#start_linkView all {stackProduct} {type}#end_link`,
+ values: {
+ type: getTypeLabelForStackProduct(missing.stackProduct),
+ stackProduct: getStackProductLabel(missing.stackProduct),
+ },
+ }),
+ getListingLinkForStackProduct(missing.stackProduct),
+ AlertMessageTokenType.Link
+ ),
+ {
+ text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.verifySettings', {
+ defaultMessage: `Verify monitoring settings on the {type}`,
+ values: {
+ type: getTypeLabelForStackProduct(missing.stackProduct, false),
+ },
+ }),
+ },
+ ],
+ tokens: [
+ {
+ startToken: '#absolute',
+ type: AlertMessageTokenType.Time,
+ isAbsolute: true,
+ isRelative: false,
+ timestamp: alertState.ui.triggeredMS,
+ } as AlertMessageTimeToken,
+ ],
+ };
+ }
+
+ protected executeActions(
+ instance: AlertInstance,
+ instanceState: AlertInstanceState,
+ item: AlertData | null,
+ cluster: AlertCluster
+ ) {
+ if (instanceState.alertStates.length === 0) {
+ return;
+ }
+
+ const ccs = instanceState.alertStates.reduce((accum: string, state): string => {
+ if (state.ccs) {
+ return state.ccs;
+ }
+ return accum;
+ }, '');
+
+ const firingCount = instanceState.alertStates.filter((alertState) => alertState.ui.isFiring)
+ .length;
+ const firingStackProducts = instanceState.alertStates
+ .filter((_state) => (_state as AlertMissingDataState).ui.isFiring)
+ .map((_state) => {
+ const state = _state as AlertMissingDataState;
+ return `${getStackProductLabel(state.stackProduct)} ${getTypeLabelForStackProduct(
+ state.stackProduct,
+ false
+ )}: ${state.stackProductName}`;
+ })
+ .join(', ');
+ if (firingCount > 0) {
+ const shortActionText = i18n.translate('xpack.monitoring.alerts.missingData.shortAction', {
+ defaultMessage:
+ 'Verify these stack products are up and running, then double check the monitoring settings.',
+ });
+ const fullActionText = i18n.translate('xpack.monitoring.alerts.missingData.fullAction', {
+ defaultMessage: 'View what monitoring data we do have for these stack products.',
+ });
+ const globalState = [`cluster_uuid:${cluster.clusterUuid}`];
+ if (ccs) {
+ globalState.push(`ccs:${ccs}`);
+ }
+ const url = `${this.kibanaUrl}/app/monitoring#overview?_g=(${globalState.join(',')})`;
+ const action = `[${fullActionText}](${url})`;
+ const internalShortMessage = i18n.translate(
+ 'xpack.monitoring.alerts.missingData.firing.internalShortMessage',
+ {
+ defaultMessage: `We have not detected any monitoring data for {count} stack product(s) in cluster: {clusterName}. {shortActionText}`,
+ values: {
+ count: firingCount,
+ clusterName: cluster.clusterName,
+ shortActionText,
+ },
+ }
+ );
+ const internalFullMessage = i18n.translate(
+ 'xpack.monitoring.alerts.missingData.firing.internalFullMessage',
+ {
+ defaultMessage: `We have not detected any monitoring data for {count} stack product(s) in cluster: {clusterName}. {action}`,
+ values: {
+ count: firingCount,
+ clusterName: cluster.clusterName,
+ action,
+ },
+ }
+ );
+ instance.scheduleActions('default', {
+ internalShortMessage,
+ internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage,
+ state: FIRING,
+ stackProducts: firingStackProducts,
+ count: firingCount,
+ clusterName: cluster.clusterName,
+ action,
+ actionPlain: shortActionText,
+ });
+ } else {
+ const resolvedCount = instanceState.alertStates.filter(
+ (alertState) => !alertState.ui.isFiring
+ ).length;
+ const resolvedStackProducts = instanceState.alertStates
+ .filter((_state) => !(_state as AlertMissingDataState).ui.isFiring)
+ .map((_state) => {
+ const state = _state as AlertMissingDataState;
+ return `${getStackProductLabel(state.stackProduct)} ${getTypeLabelForStackProduct(
+ state.stackProduct,
+ false
+ )}: ${state.stackProductName}`;
+ })
+ .join(',');
+ if (resolvedCount > 0) {
+ instance.scheduleActions('default', {
+ internalShortMessage: i18n.translate(
+ 'xpack.monitoring.alerts.missingData.resolved.internalShortMessage',
+ {
+ defaultMessage: `We are now seeing monitoring data for {count} stack product(s) in cluster: {clusterName}.`,
+ values: {
+ count: resolvedCount,
+ clusterName: cluster.clusterName,
+ },
+ }
+ ),
+ internalFullMessage: i18n.translate(
+ 'xpack.monitoring.alerts.missingData.resolved.internalFullMessage',
+ {
+ defaultMessage: `We are now seeing monitoring data for {count} stack product(s) in cluster {clusterName}.`,
+ values: {
+ count: resolvedCount,
+ clusterName: cluster.clusterName,
+ },
+ }
+ ),
+ state: RESOLVED,
+ stackProducts: resolvedStackProducts,
+ count: resolvedCount,
+ clusterName: cluster.clusterName,
+ });
+ }
+ }
+ }
+
+ protected async processData(
+ data: AlertData[],
+ clusters: AlertCluster[],
+ services: AlertServices,
+ logger: Logger
+ ) {
+ for (const cluster of clusters) {
+ const stackProducts = data.filter((_item) => _item.clusterUuid === cluster.clusterUuid);
+ if (stackProducts.length === 0) {
+ continue;
+ }
+
+ const firingInstances = stackProducts.reduce((list: string[], stackProduct) => {
+ const { missing } = stackProduct.meta as { missing: AlertMissingData; limit: number };
+ if (stackProduct.shouldFire) {
+ list.push(`${missing.stackProduct}:${missing.stackProductUuid}`);
+ }
+ return list;
+ }, [] as string[]);
+ firingInstances.sort(); // It doesn't matter how we sort, but keep the order consistent
+ const instanceId = `${this.type}:${cluster.clusterUuid}:${firingInstances.join(',')}`;
+ const instance = services.alertInstanceFactory(instanceId);
+ const instanceState = (instance.getState() as unknown) as AlertInstanceState;
+ const alertInstanceState: AlertInstanceState = {
+ alertStates: instanceState?.alertStates || [],
+ };
+ let shouldExecuteActions = false;
+ for (const stackProduct of stackProducts) {
+ const { missing } = stackProduct.meta as { missing: AlertMissingData; limit: number };
+ let state: AlertMissingDataState;
+ const indexInState = alertInstanceState.alertStates.findIndex((alertState) => {
+ const _alertState = alertState as AlertMissingDataState;
+ return (
+ _alertState.cluster.clusterUuid === cluster.clusterUuid &&
+ _alertState.stackProduct === missing.stackProduct &&
+ _alertState.stackProductUuid === missing.stackProductUuid
+ );
+ });
+ if (indexInState > -1) {
+ state = alertInstanceState.alertStates[indexInState] as AlertMissingDataState;
+ } else {
+ state = this.getDefaultAlertState(cluster, stackProduct) as AlertMissingDataState;
+ }
+
+ state.stackProduct = missing.stackProduct;
+ state.stackProductUuid = missing.stackProductUuid;
+ state.stackProductName = missing.stackProductName;
+ state.gapDuration = missing.gapDuration;
+
+ if (stackProduct.shouldFire) {
+ if (!state.ui.isFiring) {
+ state.ui.triggeredMS = new Date().valueOf();
+ }
+ state.ui.isFiring = true;
+ state.ui.message = this.getUiMessage(state, stackProduct);
+ state.ui.severity = stackProduct.severity;
+ state.ui.resolvedMS = 0;
+ shouldExecuteActions = true;
+ } else if (!stackProduct.shouldFire && state.ui.isFiring) {
+ state.ui.isFiring = false;
+ state.ui.resolvedMS = new Date().valueOf();
+ state.ui.message = this.getUiMessage(state, stackProduct);
+ shouldExecuteActions = true;
+ }
+
+ if (indexInState === -1) {
+ alertInstanceState.alertStates.push(state);
+ } else {
+ alertInstanceState.alertStates = [
+ ...alertInstanceState.alertStates.slice(0, indexInState),
+ state,
+ ...alertInstanceState.alertStates.slice(indexInState + 1),
+ ];
+ }
+ }
+
+ instance.replaceState(alertInstanceState);
+ if (shouldExecuteActions) {
+ this.executeActions(instance, alertInstanceState, null, cluster);
+ }
+ }
+ }
+}
diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts
index b685dcaed790f5..4b78bca9f47ca6 100644
--- a/x-pack/plugins/monitoring/server/alerts/types.d.ts
+++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts
@@ -28,6 +28,13 @@ export interface AlertCpuUsageState extends AlertState {
nodeName: string;
}
+export interface AlertMissingDataState extends AlertState {
+ stackProduct: string;
+ stackProductUuid: string;
+ stackProductName: string;
+ gapDuration: number;
+}
+
export interface AlertDiskUsageState extends AlertState {
diskUsage: number;
nodeId: string;
@@ -93,6 +100,15 @@ export interface AlertDiskUsageNodeStats {
ccs?: string;
}
+export interface AlertMissingData {
+ stackProduct: string;
+ stackProductUuid: string;
+ stackProductName: string;
+ clusterUuid: string;
+ gapDuration: number;
+ ccs?: string;
+}
+
export interface AlertData {
instanceKey: string;
clusterUuid: string;
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts
index 48ad31d20a3951..d474338bce9229 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts
@@ -6,7 +6,18 @@
import { get } from 'lodash';
import { AlertCluster } from '../../alerts/types';
-export async function fetchClusters(callCluster: any, index: string): Promise {
+interface RangeFilter {
+ [field: string]: {
+ format?: string;
+ gte: string | number;
+ };
+}
+
+export async function fetchClusters(
+ callCluster: any,
+ index: string,
+ rangeFilter: RangeFilter = { timestamp: { gte: 'now-2m' } }
+): Promise {
const params = {
index,
filterPath: [
@@ -25,11 +36,7 @@ export async function fetchClusters(callCluster: any, index: string): Promise
+) {
+ return {
+ buckets: products.map((product) => {
+ return {
+ key: product.uuid,
+ most_recent: {
+ value: product.timestamp,
+ },
+ document: {
+ hits: {
+ hits: [
+ {
+ _index: index,
+ _source: product.nameSource,
+ },
+ ],
+ },
+ },
+ };
+ }),
+ };
+}
+
+describe('fetchMissingMonitoringData', () => {
+ let callCluster = jest.fn();
+ const index = '.monitoring-*';
+ const startMs = 100;
+ const size = 10;
+
+ it('fetch as expected', async () => {
+ const now = 10;
+ const clusters = [
+ {
+ clusterUuid: 'clusterUuid1',
+ clusterName: 'clusterName1',
+ },
+ ];
+ callCluster = jest.fn().mockImplementation((...args) => {
+ return {
+ aggregations: {
+ clusters: {
+ buckets: clusters.map((cluster) => ({
+ key: cluster.clusterUuid,
+ es_uuids: getResponse('.monitoring-es-*', [
+ {
+ uuid: 'nodeUuid1',
+ nameSource: {
+ source_node: {
+ name: 'nodeName1',
+ },
+ },
+ timestamp: 9,
+ },
+ {
+ uuid: 'nodeUuid2',
+ nameSource: {
+ source_node: {
+ name: 'nodeName2',
+ },
+ },
+ timestamp: 2,
+ },
+ ]),
+ kibana_uuids: getResponse('.monitoring-kibana-*', [
+ {
+ uuid: 'kibanaUuid1',
+ nameSource: {
+ kibana_stats: {
+ kibana: {
+ name: 'kibanaName1',
+ },
+ },
+ },
+ timestamp: 4,
+ },
+ ]),
+ logstash_uuids: getResponse('.monitoring-logstash-*', [
+ {
+ uuid: 'logstashUuid1',
+ nameSource: {
+ logstash_stats: {
+ logstash: {
+ host: 'logstashName1',
+ },
+ },
+ },
+ timestamp: 2,
+ },
+ ]),
+ beats: {
+ beats_uuids: getResponse('.monitoring-beats-*', [
+ {
+ uuid: 'beatUuid1',
+ nameSource: {
+ beats_stats: {
+ beat: {
+ name: 'beatName1',
+ },
+ },
+ },
+ timestamp: 0,
+ },
+ ]),
+ },
+ apms: {
+ apm_uuids: getResponse('.monitoring-beats-*', [
+ {
+ uuid: 'apmUuid1',
+ nameSource: {
+ beats_stats: {
+ beat: {
+ name: 'apmName1',
+ type: 'apm-server',
+ },
+ },
+ },
+ timestamp: 1,
+ },
+ ]),
+ },
+ })),
+ },
+ },
+ };
+ });
+ const result = await fetchMissingMonitoringData(
+ callCluster,
+ clusters,
+ index,
+ size,
+ now,
+ startMs
+ );
+ expect(result).toEqual([
+ {
+ stackProduct: 'elasticsearch',
+ stackProductUuid: 'nodeUuid1',
+ stackProductName: 'nodeName1',
+ clusterUuid: 'clusterUuid1',
+ gapDuration: 1,
+ ccs: null,
+ },
+ {
+ stackProduct: 'elasticsearch',
+ stackProductUuid: 'nodeUuid2',
+ stackProductName: 'nodeName2',
+ clusterUuid: 'clusterUuid1',
+ gapDuration: 8,
+ ccs: null,
+ },
+ {
+ stackProduct: 'kibana',
+ stackProductUuid: 'kibanaUuid1',
+ stackProductName: 'kibanaName1',
+ clusterUuid: 'clusterUuid1',
+ gapDuration: 6,
+ ccs: null,
+ },
+ {
+ stackProduct: 'logstash',
+ stackProductUuid: 'logstashUuid1',
+ stackProductName: 'logstashName1',
+ clusterUuid: 'clusterUuid1',
+ gapDuration: 8,
+ ccs: null,
+ },
+ {
+ stackProduct: 'beats',
+ stackProductUuid: 'beatUuid1',
+ stackProductName: 'beatName1',
+ clusterUuid: 'clusterUuid1',
+ gapDuration: 10,
+ ccs: null,
+ },
+ {
+ stackProduct: 'apm',
+ stackProductUuid: 'apmUuid1',
+ stackProductName: 'apmName1',
+ clusterUuid: 'clusterUuid1',
+ gapDuration: 9,
+ ccs: null,
+ },
+ ]);
+ });
+
+ it('should handle ccs', async () => {
+ const now = 10;
+ const clusters = [
+ {
+ clusterUuid: 'clusterUuid1',
+ clusterName: 'clusterName1',
+ },
+ ];
+ callCluster = jest.fn().mockImplementation((...args) => {
+ return {
+ aggregations: {
+ clusters: {
+ buckets: clusters.map((cluster) => ({
+ key: cluster.clusterUuid,
+ es_uuids: getResponse('Monitoring:.monitoring-es-*', [
+ {
+ uuid: 'nodeUuid1',
+ nameSource: {
+ source_node: {
+ name: 'nodeName1',
+ },
+ },
+ timestamp: 9,
+ },
+ ]),
+ })),
+ },
+ },
+ };
+ });
+ const result = await fetchMissingMonitoringData(
+ callCluster,
+ clusters,
+ index,
+ size,
+ now,
+ startMs
+ );
+ expect(result).toEqual([
+ {
+ stackProduct: 'elasticsearch',
+ stackProductUuid: 'nodeUuid1',
+ stackProductName: 'nodeName1',
+ clusterUuid: 'clusterUuid1',
+ gapDuration: 1,
+ ccs: 'Monitoring',
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
new file mode 100644
index 00000000000000..91fc05137a8c18
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
@@ -0,0 +1,275 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { get } from 'lodash';
+import { AlertCluster, AlertMissingData } from '../../alerts/types';
+import {
+ KIBANA_SYSTEM_ID,
+ BEATS_SYSTEM_ID,
+ APM_SYSTEM_ID,
+ LOGSTASH_SYSTEM_ID,
+ ELASTICSEARCH_SYSTEM_ID,
+} from '../../../common/constants';
+
+interface ClusterBucketESResponse {
+ key: string;
+ kibana_uuids?: UuidResponse;
+ logstash_uuids?: UuidResponse;
+ es_uuids?: UuidResponse;
+ beats?: {
+ beats_uuids: UuidResponse;
+ };
+ apms?: {
+ apm_uuids: UuidResponse;
+ };
+}
+
+interface UuidResponse {
+ buckets: UuidBucketESResponse[];
+}
+
+interface UuidBucketESResponse {
+ key: string;
+ most_recent: {
+ value: number;
+ };
+ document: {
+ hits: {
+ hits: TopHitESResponse[];
+ };
+ };
+}
+
+interface TopHitESResponse {
+ _index: string;
+ _source: {
+ source_node?: {
+ name: string;
+ };
+ kibana_stats?: {
+ kibana: {
+ name: string;
+ };
+ };
+ logstash_stats?: {
+ logstash: {
+ host: string;
+ };
+ };
+ beats_stats?: {
+ beat: {
+ name: string;
+ type: string;
+ };
+ };
+ };
+}
+
+function getStackProductFromIndex(index: string, beatType: string) {
+ if (index.includes('-kibana-')) {
+ return KIBANA_SYSTEM_ID;
+ }
+ if (index.includes('-beats-')) {
+ if (beatType === 'apm-server') {
+ return APM_SYSTEM_ID;
+ }
+ return BEATS_SYSTEM_ID;
+ }
+ if (index.includes('-logstash-')) {
+ return LOGSTASH_SYSTEM_ID;
+ }
+ if (index.includes('-es-')) {
+ return ELASTICSEARCH_SYSTEM_ID;
+ }
+ return '';
+}
+
+export async function fetchMissingMonitoringData(
+ callCluster: any,
+ clusters: AlertCluster[],
+ index: string,
+ size: number,
+ nowInMs: number,
+ startMs: number
+): Promise {
+ const endMs = nowInMs;
+
+ const nameFields = [
+ 'source_node.name',
+ 'kibana_stats.kibana.name',
+ 'logstash_stats.logstash.host',
+ 'beats_stats.beat.name',
+ 'beat_stats.beat.type',
+ ];
+ const subAggs = {
+ most_recent: {
+ max: {
+ field: 'timestamp',
+ },
+ },
+ document: {
+ top_hits: {
+ size: 1,
+ sort: [
+ {
+ timestamp: {
+ order: 'desc',
+ },
+ },
+ ],
+ _source: {
+ includes: ['_index', ...nameFields],
+ },
+ },
+ },
+ };
+
+ const params = {
+ index,
+ filterPath: ['aggregations.clusters.buckets'],
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ {
+ terms: {
+ cluster_uuid: clusters.map((cluster) => cluster.clusterUuid),
+ },
+ },
+ {
+ range: {
+ timestamp: {
+ format: 'epoch_millis',
+ gte: startMs,
+ lte: endMs,
+ },
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ clusters: {
+ terms: {
+ field: 'cluster_uuid',
+ size,
+ },
+ aggs: {
+ es_uuids: {
+ terms: {
+ field: 'node_stats.node_id',
+ size,
+ },
+ aggs: subAggs,
+ },
+ kibana_uuids: {
+ terms: {
+ field: 'kibana_stats.kibana.uuid',
+ size,
+ },
+ aggs: subAggs,
+ },
+ beats: {
+ filter: {
+ bool: {
+ must_not: {
+ term: {
+ 'beats_stats.beat.type': 'apm-server',
+ },
+ },
+ },
+ },
+ aggs: {
+ beats_uuids: {
+ terms: {
+ field: 'beats_stats.beat.uuid',
+ size,
+ },
+ aggs: subAggs,
+ },
+ },
+ },
+ apms: {
+ filter: {
+ bool: {
+ must: {
+ term: {
+ 'beats_stats.beat.type': 'apm-server',
+ },
+ },
+ },
+ },
+ aggs: {
+ apm_uuids: {
+ terms: {
+ field: 'beats_stats.beat.uuid',
+ size,
+ },
+ aggs: subAggs,
+ },
+ },
+ },
+ logstash_uuids: {
+ terms: {
+ field: 'logstash_stats.logstash.uuid',
+ size,
+ },
+ aggs: subAggs,
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const response = await callCluster('search', params);
+ const clusterBuckets = get(
+ response,
+ 'aggregations.clusters.buckets',
+ []
+ ) as ClusterBucketESResponse[];
+ const uniqueList: { [id: string]: AlertMissingData } = {};
+ for (const clusterBucket of clusterBuckets) {
+ const clusterUuid = clusterBucket.key;
+
+ const uuidBuckets = [
+ ...(clusterBucket.es_uuids?.buckets || []),
+ ...(clusterBucket.kibana_uuids?.buckets || []),
+ ...(clusterBucket.logstash_uuids?.buckets || []),
+ ...(clusterBucket.beats?.beats_uuids.buckets || []),
+ ...(clusterBucket.apms?.apm_uuids.buckets || []),
+ ];
+
+ for (const uuidBucket of uuidBuckets) {
+ const stackProductUuid = uuidBucket.key;
+ const indexName = get(uuidBucket, `document.hits.hits[0]._index`);
+ const stackProduct = getStackProductFromIndex(
+ indexName,
+ get(uuidBucket, `document.hits.hits[0]._source.beats_stats.beat.type`)
+ );
+ const differenceInMs = nowInMs - uuidBucket.most_recent.value;
+ let stackProductName = stackProductUuid;
+ for (const nameField of nameFields) {
+ stackProductName = get(uuidBucket, `document.hits.hits[0]._source.${nameField}`);
+ if (stackProductName) {
+ break;
+ }
+ }
+
+ uniqueList[`${clusterUuid}${stackProduct}${stackProductUuid}`] = {
+ stackProduct,
+ stackProductUuid,
+ stackProductName,
+ clusterUuid,
+ gapDuration: differenceInMs,
+ ccs: indexName.includes(':') ? indexName.split(':')[0] : null,
+ };
+ }
+ }
+
+ const missingData = Object.values(uniqueList);
+ return missingData;
+}
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_listing_link_for_stack_product.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_listing_link_for_stack_product.ts
new file mode 100644
index 00000000000000..1936ac1bc6183a
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/lib/alerts/get_listing_link_for_stack_product.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import {
+ BEATS_SYSTEM_ID,
+ ELASTICSEARCH_SYSTEM_ID,
+ KIBANA_SYSTEM_ID,
+ LOGSTASH_SYSTEM_ID,
+ APM_SYSTEM_ID,
+} from '../../../common/constants';
+
+export function getListingLinkForStackProduct(stackProduct: string) {
+ switch (stackProduct) {
+ case ELASTICSEARCH_SYSTEM_ID:
+ return 'elasticsearch/nodes';
+ case LOGSTASH_SYSTEM_ID:
+ return 'logstash/nodes';
+ case KIBANA_SYSTEM_ID:
+ return 'kibana/instances';
+ case BEATS_SYSTEM_ID:
+ return 'beats/beats';
+ case APM_SYSTEM_ID:
+ return 'apm/instances';
+ }
+ return '';
+}
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_stack_product_label.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_stack_product_label.ts
new file mode 100644
index 00000000000000..9dafd775bac144
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/lib/alerts/get_stack_product_label.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { capitalize } from 'lodash';
+import { APM_SYSTEM_ID, BEATS_SYSTEM_ID } from '../../../common/constants';
+
+export function getStackProductLabel(stackProduct: string) {
+ switch (stackProduct) {
+ case APM_SYSTEM_ID:
+ return 'APM';
+ case BEATS_SYSTEM_ID:
+ return 'Beat';
+ }
+ return capitalize(stackProduct);
+}
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_type_label_for_stack_product.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_type_label_for_stack_product.ts
new file mode 100644
index 00000000000000..74801de10438f5
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/lib/alerts/get_type_label_for_stack_product.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { i18n } from '@kbn/i18n';
+import {
+ BEATS_SYSTEM_ID,
+ ELASTICSEARCH_SYSTEM_ID,
+ KIBANA_SYSTEM_ID,
+ LOGSTASH_SYSTEM_ID,
+ APM_SYSTEM_ID,
+} from '../../../common/constants';
+
+const NODES = i18n.translate('xpack.monitoring.alerts.typeLabel.nodes', {
+ defaultMessage: 'nodes',
+});
+
+const INSTANCES = i18n.translate('xpack.monitoring.alerts.typeLabel.instances', {
+ defaultMessage: 'instances',
+});
+
+const SERVERS = i18n.translate('xpack.monitoring.alerts.typeLabel.servers', {
+ defaultMessage: 'servers',
+});
+
+const NODE = i18n.translate('xpack.monitoring.alerts.typeLabel.node', {
+ defaultMessage: 'node',
+});
+
+const INSTANCE = i18n.translate('xpack.monitoring.alerts.typeLabel.instance', {
+ defaultMessage: 'instance',
+});
+
+const SERVER = i18n.translate('xpack.monitoring.alerts.typeLabel.server', {
+ defaultMessage: 'server',
+});
+
+export function getTypeLabelForStackProduct(stackProduct: string, plural: boolean = true) {
+ switch (stackProduct) {
+ case ELASTICSEARCH_SYSTEM_ID:
+ case LOGSTASH_SYSTEM_ID:
+ return plural ? NODES : NODE;
+ case KIBANA_SYSTEM_ID:
+ case BEATS_SYSTEM_ID:
+ return plural ? INSTANCES : INSTANCE;
+ case APM_SYSTEM_ID:
+ return plural ? SERVERS : SERVER;
+ }
+ return 'n/a';
+}
From 7836998e76e64cdc15bb3233f2a113222837a80d Mon Sep 17 00:00:00 2001
From: Zacqary Adam Xeper
Date: Thu, 1 Oct 2020 11:31:39 -0500
Subject: [PATCH 041/128] [Metrics UI] Display No Data context.values as [NO
DATA] (#78038)
Co-authored-by: Elastic Machine
---
.../inventory_metric_threshold_executor.ts | 6 ++++--
.../metric_threshold_executor.ts | 17 +++++++++++++++--
2 files changed, 19 insertions(+), 4 deletions(-)
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 99904f15b46061..b56ede19743939 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
@@ -148,8 +148,10 @@ export const FIRED_ACTIONS = {
const formatMetric = (metric: SnapshotMetricType, value: number) => {
const metricFormatter = get(METRIC_FORMATTERS, metric, METRIC_FORMATTERS.count);
- if (value == null) {
- return '';
+ if (isNaN(value)) {
+ return i18n.translate('xpack.infra.metrics.alerting.inventory.noDataFormattedValue', {
+ defaultMessage: '[NO DATA]',
+ });
}
const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template);
return formatter(value);
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 c85685b4cdca8e..4dec552c5bd6c6 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
@@ -131,11 +131,24 @@ const formatAlertResult = (
} & AlertResult
) => {
const { metric, currentValue, threshold } = alertResult;
- if (!metric.endsWith('.pct')) return alertResult;
+ const noDataValue = i18n.translate(
+ 'xpack.infra.metrics.alerting.threshold.noDataFormattedValue',
+ {
+ defaultMessage: '[NO DATA]',
+ }
+ );
+ if (!metric.endsWith('.pct'))
+ return {
+ ...alertResult,
+ currentValue: currentValue ?? noDataValue,
+ };
const formatter = createFormatter('percent');
return {
...alertResult,
- currentValue: formatter(currentValue),
+ currentValue:
+ currentValue !== null && typeof currentValue !== 'undefined'
+ ? formatter(currentValue)
+ : noDataValue,
threshold: Array.isArray(threshold) ? threshold.map((v: number) => formatter(v)) : threshold,
};
};
From 963fe0c1d1ceaea53666e039dbe0863dde2c5c12 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Thu, 1 Oct 2020 13:09:02 -0400
Subject: [PATCH 042/128] [Ingest Manager] Ensure we trigger agent policy
updated event when we bump revision. (#78836)
---
.../server/services/agent_policy.ts | 18 +++++++--
.../apis/settings/update.ts | 40 +++++++++++++++++++
2 files changed, 55 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
index 29821a530098c2..12ea8ab92f6c4f 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts
@@ -33,7 +33,7 @@ const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE;
class AgentPolicyService {
private triggerAgentPolicyUpdatedEvent = async (
soClient: SavedObjectsClientContract,
- action: string,
+ action: 'created' | 'updated' | 'deleted',
agentPolicyId: string
) => {
return agentPolicyUpdateEventHandler(soClient, action, agentPolicyId);
@@ -258,7 +258,11 @@ class AgentPolicyService {
id: string,
options?: { user?: AuthenticatedUser }
): Promise {
- return this._update(soClient, id, {}, options?.user);
+ const res = await this._update(soClient, id, {}, options?.user);
+
+ await this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', id);
+
+ return res;
}
public async bumpAllAgentPolicies(
soClient: SavedObjectsClientContract,
@@ -277,7 +281,15 @@ class AgentPolicyService {
};
return policy;
});
- return soClient.bulkUpdate(bumpedPolicies);
+ const res = await soClient.bulkUpdate(bumpedPolicies);
+
+ await Promise.all(
+ currentPolicies.saved_objects.map((policy) =>
+ this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', policy.id)
+ )
+ );
+
+ return res;
}
public async assignPackagePolicies(
diff --git a/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts b/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts
index 86292b535db2d4..4340cd4832307b 100644
--- a/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts
+++ b/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts
@@ -5,16 +5,20 @@
*/
import expect from '@kbn/expect';
+import { Client } from 'elasticsearch';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
+import { setupIngest } from '../fleet/agents/services';
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
+ const esClient: Client = getService('legacyEs');
describe('Settings - update', async function () {
skipIfNoDockerRegistry(providerContext);
+ setupIngest(providerContext);
it("should bump all agent policy's revision", async function () {
const { body: testPolicy1PostRes } = await supertest
@@ -49,5 +53,41 @@ export default function (providerContext: FtrProviderContext) {
expect(getTestPolicy1Res.attributes.revision).equal(2);
expect(getTestPolicy2Res.attributes.revision).equal(2);
});
+
+ it('should create agent actions', async function () {
+ const { body: testPolicyRes } = await supertest
+ .post(`/api/ingest_manager/agent_policies`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'test',
+ description: '',
+ namespace: 'default',
+ });
+
+ await supertest
+ .put(`/api/ingest_manager/settings`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({ kibana_urls: ['http://localhost:1232/abc', 'http://localhost:1232/abc'] });
+
+ const res = await esClient.search({
+ index: '.kibana',
+ body: {
+ query: {
+ bool: {
+ must: [
+ {
+ terms: {
+ type: ['fleet-agent-actions'],
+ },
+ },
+ { match: { 'fleet-agent-actions.policy_id': testPolicyRes.item.id } },
+ ],
+ },
+ },
+ },
+ });
+
+ expect(res.hits.hits.length).equal(2);
+ });
});
}
From 574205dc72b63a3c30ff159684012d8e3191ef2d Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 1 Oct 2020 18:14:59 +0100
Subject: [PATCH 043/128] chore(NA): remove non existing plugin paths from case
api integration tests (#79127)
* chore(NA): remove non existing plugin paths from case api integration tests config
* chore(NA): remove unused import
---
x-pack/test/case_api_integration/common/config.ts | 4 ----
1 file changed, 4 deletions(-)
diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts
index 5d34f8b04981a0..72d1bc4ec9a37a 100644
--- a/x-pack/test/case_api_integration/common/config.ts
+++ b/x-pack/test/case_api_integration/common/config.ts
@@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import path from 'path';
-
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
@@ -78,8 +76,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
'--xpack.eventLog.logEntries=true',
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
- `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
- `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`,
...(ssl
? [
`--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
From fd7dd41617a7464e6b0e61a83800c05ccf6a5189 Mon Sep 17 00:00:00 2001
From: Quynh Nguyen <43350163+qn895@users.noreply.github.com>
Date: Thu, 1 Oct 2020 12:41:12 -0500
Subject: [PATCH 044/128] [ML] Update transform cloning to include description
and new fields (#78364)
---
.../public/app/common/request.test.ts | 33 +++++++++++++
.../transform/public/app/common/request.ts | 22 ++++++---
.../step_details/step_details_form.tsx | 18 +++++++
.../step_details/step_details_summary.tsx | 2 +
.../test/functional/apps/transform/cloning.ts | 15 +++++-
.../functional/services/transform/wizard.ts | 47 +++++++++++++++++++
6 files changed, 129 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts
index 913ea8964eaf0e..46ace2c3315a5a 100644
--- a/x-pack/plugins/transform/public/app/common/request.test.ts
+++ b/x-pack/plugins/transform/public/app/common/request.test.ts
@@ -17,6 +17,7 @@ import {
defaultQuery,
getPreviewTransformRequestBody,
getCreateTransformRequestBody,
+ getCreateTransformSettingsRequestBody,
getPivotQuery,
isDefaultQuery,
isMatchAllQuery,
@@ -159,6 +160,7 @@ describe('Transform: Common', () => {
transformDescription: 'the-transform-description',
transformFrequency: '1m',
transformSettingsMaxPageSearchSize: 100,
+ transformSettingsDocsPerSecond: 400,
destinationIndex: 'the-destination-index',
touched: true,
valid: true,
@@ -180,6 +182,7 @@ describe('Transform: Common', () => {
},
settings: {
max_page_search_size: 100,
+ docs_per_second: 400,
},
source: {
index: ['the-index-pattern-title'],
@@ -187,4 +190,34 @@ describe('Transform: Common', () => {
},
});
});
+
+ test('getCreateTransformSettingsRequestBody() with multiple settings', () => {
+ const transformDetailsState: Partial = {
+ transformSettingsDocsPerSecond: 400,
+ transformSettingsMaxPageSearchSize: 100,
+ };
+
+ const request = getCreateTransformSettingsRequestBody(transformDetailsState);
+
+ expect(request).toEqual({
+ settings: {
+ docs_per_second: 400,
+ max_page_search_size: 100,
+ },
+ });
+ });
+
+ test('getCreateTransformSettingsRequestBody() with one setting', () => {
+ const transformDetailsState: Partial = {
+ transformSettingsDocsPerSecond: 400,
+ };
+
+ const request = getCreateTransformSettingsRequestBody(transformDetailsState);
+
+ expect(request).toEqual({
+ settings: {
+ docs_per_second: 400,
+ },
+ });
+ });
});
diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts
index 45160d125309db..8ee235baf7c5aa 100644
--- a/x-pack/plugins/transform/public/app/common/request.ts
+++ b/x-pack/plugins/transform/public/app/common/request.ts
@@ -130,6 +130,20 @@ export function getPreviewTransformRequestBody(
return request;
}
+export const getCreateTransformSettingsRequestBody = (
+ transformDetailsState: Partial
+): { settings?: PutTransformsRequestSchema['settings'] } => {
+ const settings: PutTransformsRequestSchema['settings'] = {
+ ...(transformDetailsState.transformSettingsMaxPageSearchSize
+ ? { max_page_search_size: transformDetailsState.transformSettingsMaxPageSearchSize }
+ : {}),
+ ...(transformDetailsState.transformSettingsDocsPerSecond
+ ? { docs_per_second: transformDetailsState.transformSettingsDocsPerSecond }
+ : {}),
+ };
+ return Object.keys(settings).length > 0 ? { settings } : {};
+};
+
export const getCreateTransformRequestBody = (
indexPatternTitle: IndexPattern['title'],
pivotState: StepDefineExposedState,
@@ -164,13 +178,7 @@ export const getCreateTransformRequestBody = (
}
: {}),
// conditionally add additional settings
- ...(transformDetailsState.transformSettingsMaxPageSearchSize
- ? {
- settings: {
- max_page_search_size: transformDetailsState.transformSettingsMaxPageSearchSize,
- },
- }
- : {}),
+ ...getCreateTransformSettingsRequestBody(transformDetailsState),
});
export function isHttpFetchError(error: any): error is HttpFetchError {
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
index 00ab516f625fe9..9b43879512e4db 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
@@ -63,6 +63,7 @@ export interface StepDetailsExposedState {
transformDescription: string;
transformFrequency: string;
transformSettingsMaxPageSearchSize: number;
+ transformSettingsDocsPerSecond?: number;
valid: boolean;
indexPatternTimeField?: string | undefined;
}
@@ -100,6 +101,20 @@ export function applyTransformConfigToDetailsState(
state.continuousModeDelay = time?.delay ?? defaultContinuousModeDelay;
state.isContinuousModeEnabled = true;
}
+ if (transformConfig.description !== undefined) {
+ state.transformDescription = transformConfig.description;
+ }
+ if (transformConfig.frequency !== undefined) {
+ state.transformFrequency = transformConfig.frequency;
+ }
+ if (transformConfig.settings) {
+ if (typeof transformConfig.settings?.max_page_search_size === 'number') {
+ state.transformSettingsMaxPageSearchSize = transformConfig.settings.max_page_search_size;
+ }
+ if (typeof transformConfig.settings?.docs_per_second === 'number') {
+ state.transformSettingsDocsPerSecond = transformConfig.settings.docs_per_second;
+ }
+ }
}
return state;
}
@@ -275,6 +290,8 @@ export const StepDetailsForm: FC = React.memo(
const [transformSettingsMaxPageSearchSize, setTransformSettingsMaxPageSearchSize] = useState(
defaults.transformSettingsMaxPageSearchSize
);
+ const [transformSettingsDocsPerSecond] = useState(defaults.transformSettingsDocsPerSecond);
+
const isTransformSettingsMaxPageSearchSizeValid = transformSettingsMaxPageSearchSizeValidator(
transformSettingsMaxPageSearchSize
);
@@ -301,6 +318,7 @@ export const StepDetailsForm: FC = React.memo(
transformDescription,
transformFrequency,
transformSettingsMaxPageSearchSize,
+ transformSettingsDocsPerSecond,
destinationIndex,
touched: true,
valid,
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx
index 45cd8aa465522c..f5444eaf6640af 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx
@@ -98,6 +98,7 @@ export const StepDetailsSummary: FC = React.memo((props
paddingSize="s"
>
= React.memo((props
{transformFrequency}
Date: Thu, 1 Oct 2020 12:42:37 -0500
Subject: [PATCH 045/128] Revert "[Metrics UI] Add ability to override
datafeeds and job config for partition field (#78875)"
This reverts commit ee7672aaf074dc4ebaf0ffb88d95d5f1bf9e1d18.
---
.../containers/ml/infra_ml_module_types.ts | 4 +-
.../containers/ml/infra_ml_setup_state.ts | 289 ++++++++++++++++++
.../metrics_hosts/module_descriptor.ts | 135 +++-----
.../modules/metrics_k8s/module_descriptor.ts | 143 +++------
.../anomoly_detection_flyout.tsx | 4 +-
.../ml/anomaly_detection/flyout_home.tsx | 113 ++++---
.../ml/anomaly_detection/job_setup_screen.tsx | 3 +-
7 files changed, 444 insertions(+), 247 deletions(-)
create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts
index e36f38add641aa..a9f2671de82598 100644
--- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts
@@ -33,11 +33,11 @@ export interface ModuleDescriptor {
partitionField?: string
) => Promise;
cleanUpModule: (spaceId: string, sourceId: string) => Promise;
- validateSetupIndices?: (
+ validateSetupIndices: (
indices: string[],
timestampField: string
) => Promise;
- validateSetupDatasets?: (
+ validateSetupDatasets: (
indices: string[],
timestampField: string,
startTime: number,
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts
new file mode 100644
index 00000000000000..0dfe3b301f2400
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts
@@ -0,0 +1,289 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { isEqual } from 'lodash';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { usePrevious } from 'react-use';
+import {
+ combineDatasetFilters,
+ DatasetFilter,
+ filterDatasetFilter,
+ isExampleDataIndex,
+} from '../../../common/infra_ml';
+import {
+ AvailableIndex,
+ ValidationIndicesError,
+ ValidationUIError,
+} from '../../components/logging/log_analysis_setup/initial_configuration_step';
+import { useTrackedPromise } from '../../utils/use_tracked_promise';
+import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types';
+
+type SetupHandler = (
+ indices: string[],
+ startTime: number | undefined,
+ endTime: number | undefined,
+ datasetFilter: DatasetFilter
+) => void;
+
+interface AnalysisSetupStateArguments {
+ cleanUpAndSetUpModule: SetupHandler;
+ moduleDescriptor: ModuleDescriptor;
+ setUpModule: SetupHandler;
+ sourceConfiguration: ModuleSourceConfiguration;
+}
+
+const fourWeeksInMs = 86400000 * 7 * 4;
+
+export const useAnalysisSetupState = ({
+ cleanUpAndSetUpModule,
+ moduleDescriptor: { validateSetupDatasets, validateSetupIndices },
+ setUpModule,
+ sourceConfiguration,
+}: AnalysisSetupStateArguments) => {
+ const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs);
+ const [endTime, setEndTime] = useState(undefined);
+
+ const isTimeRangeValid = useMemo(
+ () => (startTime != null && endTime != null ? startTime < endTime : true),
+ [endTime, startTime]
+ );
+
+ const [validatedIndices, setValidatedIndices] = useState(
+ sourceConfiguration.indices.map((indexName) => ({
+ name: indexName,
+ validity: 'unknown' as const,
+ }))
+ );
+
+ const updateIndicesWithValidationErrors = useCallback(
+ (validationErrors: ValidationIndicesError[]) =>
+ setValidatedIndices((availableIndices) =>
+ availableIndices.map((previousAvailableIndex) => {
+ const indexValiationErrors = validationErrors.filter(
+ ({ index }) => index === previousAvailableIndex.name
+ );
+
+ if (indexValiationErrors.length > 0) {
+ return {
+ validity: 'invalid',
+ name: previousAvailableIndex.name,
+ errors: indexValiationErrors,
+ };
+ } else if (previousAvailableIndex.validity === 'valid') {
+ return {
+ ...previousAvailableIndex,
+ validity: 'valid',
+ errors: [],
+ };
+ } else {
+ return {
+ validity: 'valid',
+ name: previousAvailableIndex.name,
+ isSelected: !isExampleDataIndex(previousAvailableIndex.name),
+ availableDatasets: [],
+ datasetFilter: {
+ type: 'includeAll' as const,
+ },
+ };
+ }
+ })
+ ),
+ []
+ );
+
+ const updateIndicesWithAvailableDatasets = useCallback(
+ (availableDatasets: Array<{ indexName: string; datasets: string[] }>) =>
+ setValidatedIndices((availableIndices) =>
+ availableIndices.map((previousAvailableIndex) => {
+ if (previousAvailableIndex.validity !== 'valid') {
+ return previousAvailableIndex;
+ }
+
+ const availableDatasetsForIndex = availableDatasets.filter(
+ ({ indexName }) => indexName === previousAvailableIndex.name
+ );
+ const newAvailableDatasets = availableDatasetsForIndex.flatMap(
+ ({ datasets }) => datasets
+ );
+
+ // filter out datasets that have disappeared if this index' datasets were updated
+ const newDatasetFilter: DatasetFilter =
+ availableDatasetsForIndex.length > 0
+ ? filterDatasetFilter(previousAvailableIndex.datasetFilter, (dataset) =>
+ newAvailableDatasets.includes(dataset)
+ )
+ : previousAvailableIndex.datasetFilter;
+
+ return {
+ ...previousAvailableIndex,
+ availableDatasets: newAvailableDatasets,
+ datasetFilter: newDatasetFilter,
+ };
+ })
+ ),
+ []
+ );
+
+ const validIndexNames = useMemo(
+ () => validatedIndices.filter((index) => index.validity === 'valid').map((index) => index.name),
+ [validatedIndices]
+ );
+
+ const selectedIndexNames = useMemo(
+ () =>
+ validatedIndices
+ .filter((index) => index.validity === 'valid' && index.isSelected)
+ .map((i) => i.name),
+ [validatedIndices]
+ );
+
+ const datasetFilter = useMemo(
+ () =>
+ validatedIndices
+ .flatMap((validatedIndex) =>
+ validatedIndex.validity === 'valid'
+ ? validatedIndex.datasetFilter
+ : { type: 'includeAll' as const }
+ )
+ .reduce(combineDatasetFilters, { type: 'includeAll' as const }),
+ [validatedIndices]
+ );
+
+ const [validateIndicesRequest, validateIndices] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ return await validateSetupIndices(
+ sourceConfiguration.indices,
+ sourceConfiguration.timestampField
+ );
+ },
+ onResolve: ({ data: { errors } }) => {
+ updateIndicesWithValidationErrors(errors);
+ },
+ onReject: () => {
+ setValidatedIndices([]);
+ },
+ },
+ [sourceConfiguration.indices, sourceConfiguration.timestampField]
+ );
+
+ const [validateDatasetsRequest, validateDatasets] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ if (validIndexNames.length === 0) {
+ return { data: { datasets: [] } };
+ }
+
+ return await validateSetupDatasets(
+ validIndexNames,
+ sourceConfiguration.timestampField,
+ startTime ?? 0,
+ endTime ?? Date.now()
+ );
+ },
+ onResolve: ({ data: { datasets } }) => {
+ updateIndicesWithAvailableDatasets(datasets);
+ },
+ },
+ [validIndexNames, sourceConfiguration.timestampField, startTime, endTime]
+ );
+
+ const setUp = useCallback(() => {
+ return setUpModule(selectedIndexNames, startTime, endTime, datasetFilter);
+ }, [setUpModule, selectedIndexNames, startTime, endTime, datasetFilter]);
+
+ const cleanUpAndSetUp = useCallback(() => {
+ return cleanUpAndSetUpModule(selectedIndexNames, startTime, endTime, datasetFilter);
+ }, [cleanUpAndSetUpModule, selectedIndexNames, startTime, endTime, datasetFilter]);
+
+ const isValidating = useMemo(
+ () => validateIndicesRequest.state === 'pending' || validateDatasetsRequest.state === 'pending',
+ [validateDatasetsRequest.state, validateIndicesRequest.state]
+ );
+
+ const validationErrors = useMemo(() => {
+ if (isValidating) {
+ return [];
+ }
+
+ return [
+ // validate request status
+ ...(validateIndicesRequest.state === 'rejected' ||
+ validateDatasetsRequest.state === 'rejected'
+ ? [{ error: 'NETWORK_ERROR' as const }]
+ : []),
+ // validation request results
+ ...validatedIndices.reduce((errors, index) => {
+ return index.validity === 'invalid' && selectedIndexNames.includes(index.name)
+ ? [...errors, ...index.errors]
+ : errors;
+ }, []),
+ // index count
+ ...(selectedIndexNames.length === 0 ? [{ error: 'TOO_FEW_SELECTED_INDICES' as const }] : []),
+ // time range
+ ...(!isTimeRangeValid ? [{ error: 'INVALID_TIME_RANGE' as const }] : []),
+ ];
+ }, [
+ isValidating,
+ validateIndicesRequest.state,
+ validateDatasetsRequest.state,
+ validatedIndices,
+ selectedIndexNames,
+ isTimeRangeValid,
+ ]);
+
+ const prevStartTime = usePrevious(startTime);
+ const prevEndTime = usePrevious(endTime);
+ const prevValidIndexNames = usePrevious(validIndexNames);
+
+ useEffect(() => {
+ if (!isTimeRangeValid) {
+ return;
+ }
+
+ validateIndices();
+ }, [isTimeRangeValid, validateIndices]);
+
+ useEffect(() => {
+ if (!isTimeRangeValid) {
+ return;
+ }
+
+ if (
+ startTime !== prevStartTime ||
+ endTime !== prevEndTime ||
+ !isEqual(validIndexNames, prevValidIndexNames)
+ ) {
+ validateDatasets();
+ }
+ }, [
+ endTime,
+ isTimeRangeValid,
+ prevEndTime,
+ prevStartTime,
+ prevValidIndexNames,
+ startTime,
+ validIndexNames,
+ validateDatasets,
+ ]);
+
+ return {
+ cleanUpAndSetUp,
+ datasetFilter,
+ endTime,
+ isValidating,
+ selectedIndexNames,
+ setEndTime,
+ setStartTime,
+ setUp,
+ startTime,
+ validatedIndices,
+ setValidatedIndices,
+ validationErrors,
+ };
+};
diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts
index 711ee76d42a64f..cec87fb1144e33 100644
--- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts
+++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts
@@ -10,27 +10,17 @@ import { cleanUpJobsAndDatafeeds } from '../../infra_ml_cleanup';
import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api';
import { callGetMlModuleAPI } from '../../api/ml_get_module';
import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api';
+import { callValidateIndicesAPI } from '../../../logs/log_analysis/api/validate_indices';
+import { callValidateDatasetsAPI } from '../../../logs/log_analysis/api/validate_datasets';
import {
metricsHostsJobTypes,
getJobId,
MetricsHostsJobType,
DatasetFilter,
bucketSpan,
+ partitionField,
} from '../../../../../common/infra_ml';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import MemoryJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import MemoryDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_memory_usage.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import NetworkInJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import NetworkInDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_in.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import NetworkOutJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import NetworkOutDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_out.json';
-type JobType = 'hosts_memory_usage' | 'hosts_network_in' | 'hosts_network_out';
const moduleId = 'metrics_ui_hosts';
const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', {
defaultMessage: 'Metrics anomanly detection',
@@ -64,68 +54,23 @@ const setUpModule = async (
end: number | undefined,
datasetFilter: DatasetFilter,
{ spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration,
- partitionField?: string
+ pField?: string
) => {
const indexNamePattern = indices.join(',');
- const jobIds: JobType[] = ['hosts_memory_usage', 'hosts_network_in', 'hosts_network_out'];
-
- const jobOverrides = jobIds.map((id) => {
- const { job: defaultJobConfig } = getDefaultJobConfigs(id);
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const analysis_config = {
- ...defaultJobConfig.analysis_config,
- };
-
- if (partitionField) {
- analysis_config.detectors[0].partition_field_name = partitionField;
- if (analysis_config.influencers.indexOf(partitionField) === -1) {
- analysis_config.influencers.push(partitionField);
- }
- }
-
- return {
- job_id: id,
- data_description: {
- time_field: timestampField,
- },
- analysis_config,
- custom_settings: {
- metrics_source_config: {
- indexPattern: indexNamePattern,
- timestampField,
- bucketSpan,
- },
- },
- };
- });
-
- const datafeedOverrides = jobIds.map((id) => {
- const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id);
-
- if (!partitionField || id === 'hosts_memory_usage') {
- // Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field
- return defaultDatafeedConfig;
- }
-
- // If we have a partition field, we need to change the aggregation to do a terms agg at the top level
- const aggregations = {
- [partitionField]: {
- terms: {
- field: partitionField,
- },
- aggregations: {
- ...defaultDatafeedConfig.aggregations,
- },
+ const jobIds = ['hosts_memory_usage', 'hosts_network_in', 'hosts_network_out'];
+ const jobOverrides = jobIds.map((id) => ({
+ job_id: id,
+ data_description: {
+ time_field: timestampField,
+ },
+ custom_settings: {
+ metrics_source_config: {
+ indexPattern: indexNamePattern,
+ timestampField,
+ bucketSpan,
},
- };
-
- return {
- ...defaultDatafeedConfig,
- job_id: id,
- aggregations,
- };
- });
+ },
+ }));
return callSetupMlModuleAPI(
moduleId,
@@ -135,34 +80,36 @@ const setUpModule = async (
sourceId,
indexNamePattern,
jobOverrides,
- datafeedOverrides
+ []
);
};
-const getDefaultJobConfigs = (jobId: JobType) => {
- switch (jobId) {
- case 'hosts_memory_usage':
- return {
- datafeed: MemoryDatafeed,
- job: MemoryJob,
- };
- case 'hosts_network_in':
- return {
- datafeed: NetworkInDatafeed,
- job: NetworkInJob,
- };
- case 'hosts_network_out':
- return {
- datafeed: NetworkOutDatafeed,
- job: NetworkOutJob,
- };
- }
-};
-
const cleanUpModule = async (spaceId: string, sourceId: string) => {
return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsHostsJobTypes);
};
+const validateSetupIndices = async (indices: string[], timestampField: string) => {
+ return await callValidateIndicesAPI(indices, [
+ {
+ name: timestampField,
+ validTypes: ['date'],
+ },
+ {
+ name: partitionField,
+ validTypes: ['keyword'],
+ },
+ ]);
+};
+
+const validateSetupDatasets = async (
+ indices: string[],
+ timestampField: string,
+ startTime: number,
+ endTime: number
+) => {
+ return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime);
+};
+
export const metricHostsModule: ModuleDescriptor = {
moduleId,
moduleName,
@@ -174,4 +121,6 @@ export const metricHostsModule: ModuleDescriptor = {
getModuleDefinition,
setUpModule,
cleanUpModule,
+ validateSetupDatasets,
+ validateSetupIndices,
};
diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts
index 41c6df92fb379c..cbcff1c307af6e 100644
--- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts
+++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts
@@ -10,28 +10,17 @@ import { cleanUpJobsAndDatafeeds } from '../../infra_ml_cleanup';
import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api';
import { callGetMlModuleAPI } from '../../api/ml_get_module';
import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api';
+import { callValidateIndicesAPI } from '../../../logs/log_analysis/api/validate_indices';
+import { callValidateDatasetsAPI } from '../../../logs/log_analysis/api/validate_datasets';
import {
metricsK8SJobTypes,
getJobId,
MetricK8sJobType,
DatasetFilter,
bucketSpan,
+ partitionField,
} from '../../../../../common/infra_ml';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import MemoryJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import MemoryDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_memory_usage.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import NetworkInJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import NetworkInDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_in.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import NetworkOutJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import NetworkOutDatafeed from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_out.json';
-type JobType = 'k8s_memory_usage' | 'k8s_network_in' | 'k8s_network_out';
-export const DEFAULT_K8S_PARTITION_FIELD = 'kubernetes.namespace';
const moduleId = 'metrics_ui_k8s';
const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', {
defaultMessage: 'Metrics anomanly detection',
@@ -65,72 +54,26 @@ const setUpModule = async (
end: number | undefined,
datasetFilter: DatasetFilter,
{ spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration,
- partitionField?: string
+ pField?: string
) => {
const indexNamePattern = indices.join(',');
- const jobIds: JobType[] = ['k8s_memory_usage', 'k8s_network_in', 'k8s_network_out'];
- const jobOverrides = jobIds.map((id) => {
- const { job: defaultJobConfig } = getDefaultJobConfigs(id);
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const analysis_config = {
- ...defaultJobConfig.analysis_config,
- };
-
- if (partitionField) {
- analysis_config.detectors[0].partition_field_name = partitionField;
- if (analysis_config.influencers.indexOf(partitionField) === -1) {
- analysis_config.influencers.push(partitionField);
- }
- }
-
- return {
- job_id: id,
- data_description: {
- time_field: timestampField,
- },
- analysis_config,
- custom_settings: {
- metrics_source_config: {
- indexPattern: indexNamePattern,
- timestampField,
- bucketSpan,
- },
- },
- };
- });
-
- const datafeedOverrides = jobIds.map((id) => {
- const { datafeed: defaultDatafeedConfig } = getDefaultJobConfigs(id);
-
- if (!partitionField || id === 'k8s_memory_usage') {
- // Since the host memory usage doesn't have custom aggs, we don't need to do anything to add a partition field
- return defaultDatafeedConfig;
- }
-
- // Because the ML K8s jobs ship with a default partition field of {kubernetes.namespace}, ignore that agg and wrap it in our own agg.
- const innerAggregation =
- defaultDatafeedConfig.aggregations[DEFAULT_K8S_PARTITION_FIELD].aggregations;
-
- // If we have a partition field, we need to change the aggregation to do a terms agg to partition the data at the top level
- const aggregations = {
- [partitionField]: {
- terms: {
- field: partitionField,
- size: 25, // 25 is arbitratry and only used to keep the number of buckets to a managable level in the event that the user choose a high cardinality partition field.
- },
- aggregations: {
- ...innerAggregation,
- },
+ const jobIds = ['k8s_memory_usage', 'k8s_network_in', 'k8s_network_out'];
+ const jobOverrides = jobIds.map((id) => ({
+ job_id: id,
+ analysis_config: {
+ bucket_span: `${bucketSpan}ms`,
+ },
+ data_description: {
+ time_field: timestampField,
+ },
+ custom_settings: {
+ metrics_source_config: {
+ indexPattern: indexNamePattern,
+ timestampField,
+ bucketSpan,
},
- };
-
- return {
- ...defaultDatafeedConfig,
- job_id: id,
- aggregations,
- };
- });
+ },
+ }));
return callSetupMlModuleAPI(
moduleId,
@@ -140,34 +83,36 @@ const setUpModule = async (
sourceId,
indexNamePattern,
jobOverrides,
- datafeedOverrides
+ []
);
};
-const getDefaultJobConfigs = (jobId: JobType) => {
- switch (jobId) {
- case 'k8s_memory_usage':
- return {
- datafeed: MemoryDatafeed,
- job: MemoryJob,
- };
- case 'k8s_network_in':
- return {
- datafeed: NetworkInDatafeed,
- job: NetworkInJob,
- };
- case 'k8s_network_out':
- return {
- datafeed: NetworkOutDatafeed,
- job: NetworkOutJob,
- };
- }
-};
-
const cleanUpModule = async (spaceId: string, sourceId: string) => {
return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsK8SJobTypes);
};
+const validateSetupIndices = async (indices: string[], timestampField: string) => {
+ return await callValidateIndicesAPI(indices, [
+ {
+ name: timestampField,
+ validTypes: ['date'],
+ },
+ {
+ name: partitionField,
+ validTypes: ['keyword'],
+ },
+ ]);
+};
+
+const validateSetupDatasets = async (
+ indices: string[],
+ timestampField: string,
+ startTime: number,
+ endTime: number
+) => {
+ return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime);
+};
+
export const metricHostsModule: ModuleDescriptor = {
moduleId,
moduleName,
@@ -179,4 +124,6 @@ export const metricHostsModule: ModuleDescriptor = {
getModuleDefinition,
setUpModule,
cleanUpModule,
+ validateSetupDatasets,
+ validateSetupIndices,
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx
index b5d224910e819d..b063713fa2c971 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx
@@ -50,10 +50,10 @@ export const AnomalyDetectionFlyout = () => {
return (
<>
-
+
{showFlyout && (
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx
index 5b520084ebb744..801dff9c4a17a5 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx
@@ -5,7 +5,7 @@
*/
import React, { useState, useCallback, useEffect } from 'react';
-import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiSpacer } from '@elastic/eui';
+import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -30,7 +30,7 @@ interface Props {
}
export const FlyoutHome = (props: Props) => {
- const [tab] = useState<'jobs' | 'anomalies'>('jobs');
+ const [tab, setTab] = useState<'jobs' | 'anomalies'>('jobs');
const { goToSetup } = props;
const {
fetchJobStatus: fetchHostJobStatus,
@@ -56,10 +56,18 @@ export const FlyoutHome = (props: Props) => {
goToSetup('kubernetes');
}, [goToSetup]);
+ const goToJobs = useCallback(() => {
+ setTab('jobs');
+ }, []);
+
const jobIds = [
...(k8sJobSummaries || []).map((k) => k.id),
...(hostJobSummaries || []).map((h) => h.id),
];
+ const anomaliesUrl = useLinkProps({
+ app: 'ml',
+ pathname: `/explorer?_g=${createResultsUrl(jobIds)}`,
+ });
useEffect(() => {
if (hasInfraMLReadCapabilities) {
@@ -97,24 +105,30 @@ export const FlyoutHome = (props: Props) => {
-
-
+
+
+
+
+
+
+
+
{hostJobSummaries.length > 0 && (
<>
0}
hasK8sJobs={k8sJobSummaries.length > 0}
- jobIds={jobIds}
/>
>
@@ -137,7 +151,6 @@ export const FlyoutHome = (props: Props) => {
interface CalloutProps {
hasHostJobs: boolean;
hasK8sJobs: boolean;
- jobIds: string[];
}
const JobsEnabledCallout = (props: CalloutProps) => {
let target = '';
@@ -162,34 +175,8 @@ const JobsEnabledCallout = (props: CalloutProps) => {
pathname: '/jobs',
});
- const anomaliesUrl = useLinkProps({
- app: 'ml',
- pathname: `/explorer?_g=${createResultsUrl(props.jobIds)}`,
- });
-
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{
}
iconType="check"
/>
+
+
+
+
>
);
};
@@ -217,11 +211,30 @@ interface CreateJobTab {
const CreateJobTab = (props: CreateJobTab) => {
return (
<>
- {/* */}
+
+
+
}
// title="Hosts"
title={
@@ -232,7 +245,7 @@ const CreateJobTab = (props: CreateJobTab) => {
}
description={
}
@@ -241,7 +254,7 @@ const CreateJobTab = (props: CreateJobTab) => {
{props.hasHostJobs && (
@@ -249,7 +262,7 @@ const CreateJobTab = (props: CreateJobTab) => {
{!props.hasHostJobs && (
@@ -260,7 +273,7 @@ const CreateJobTab = (props: CreateJobTab) => {
}
title={
{
}
description={
}
@@ -279,7 +292,7 @@ const CreateJobTab = (props: CreateJobTab) => {
{props.hasK8sJobs && (
@@ -287,7 +300,7 @@ const CreateJobTab = (props: CreateJobTab) => {
{!props.hasK8sJobs && (
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx
index c327d187f6bc20..428c002da63838 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx
@@ -20,7 +20,6 @@ import { useSourceViaHttp } from '../../../../../../containers/source/use_source
import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module';
import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module';
import { FixedDatePicker } from '../../../../../../components/fixed_datepicker';
-import { DEFAULT_K8S_PARTITION_FIELD } from '../../../../../../containers/ml/modules/metrics_k8s/module_descriptor';
interface Props {
jobType: 'hosts' | 'kubernetes';
@@ -108,7 +107,7 @@ export const JobSetupScreen = (props: Props) => {
useEffect(() => {
if (props.jobType === 'kubernetes') {
- setPartitionField([DEFAULT_K8S_PARTITION_FIELD]);
+ setPartitionField(['kubernetes.namespace']);
}
}, [props.jobType]);
From 6d8f74a128973eb6e29a85921086f1975eb958d5 Mon Sep 17 00:00:00 2001
From: Jen Huang
Date: Thu, 1 Oct 2020 10:57:19 -0700
Subject: [PATCH 046/128] [Ingest Manager] Match package spec
`dataset`->`data_stream` and `config_templates`->`policy_templates` renaming
(#78699)
* Match elastic/package-spec#24 `datasets`->`data_streams` property renaming
* Match elastic/package-spec#24 `datasets.name`->`data_streams.dataset` property renaming
* Match elastic/package-spec#24 `/dataset`->`/data_stream` directory renaming
* Match elastic/package-spec#50 `config_templates`->`policy_templates` property renaming
* Update API integration test fixtures (test packages)
* Temporarily skip API integration tests
* Temporarily skip more API integration tests
* Pin to custom docker image, unskip test suites, clean up broken icon paths in test package manifests
* Skip the only (yay!) failing test suite
* Revert "Skip the only (yay!) failing test suite"
This reverts commit 3db32e2528f16e3a659939d4d2168b26e5f931d3.
* Re-skip tests and revert docker image
Co-authored-by: Elastic Machine
---
.../common/openapi/spec_oas3.json | 6 +-
.../common/services/limited_package.ts | 2 +-
.../package_to_package_policy.test.ts | 48 ++++++------
.../services/package_to_package_policy.ts | 16 ++--
.../ingest_manager/common/types/models/epm.ts | 14 ++--
x-pack/plugins/ingest_manager/dev_docs/epm.md | 4 +-
.../dev_docs/indexing_strategy.md | 30 ++++----
.../services/validate_package_policy.test..ts | 24 +++---
.../services/validate_package_policy.ts | 20 ++---
.../step_configure_package.tsx | 16 ++--
.../ingest_manager/types/index.ts | 2 +-
.../services/epm/elasticsearch/index.test.ts | 10 +--
.../services/epm/elasticsearch/index.ts | 8 +-
.../ingest_pipeline/ingest_pipelines.test.ts | 16 ++--
.../elasticsearch/ingest_pipeline/install.ts | 44 +++++------
.../epm/elasticsearch/template/install.ts | 71 +++++++++---------
.../epm/elasticsearch/template/template.ts | 24 +++---
.../elasticsearch/transform/transform.test.ts | 18 ++---
.../epm/kibana/index_pattern/install.ts | 30 ++++----
.../services/epm/packages/assets.test.ts | 16 ++--
.../server/services/epm/packages/assets.ts | 6 +-
.../server/services/epm/packages/install.ts | 4 +-
.../services/epm/registry/index.test.ts | 4 +-
.../server/services/epm/registry/index.ts | 8 +-
.../server/services/package_policy.test.ts | 20 ++---
.../server/services/package_policy.ts | 14 ++--
.../ingest_manager/server/types/index.tsx | 2 +-
.../apache_0.1.4.tar.gz | Bin 581243 -> 579555 bytes
.../elasticsearch/ilm_policy/all_assets.json | 0
.../elasticsearch/ingest_pipeline/default.yml | 0
.../ingest_pipeline/pipeline1.yml | 0
.../ingest_pipeline/pipeline2.yml | 0
.../test_logs/fields/ecs.yml | 0
.../test_logs/fields/fields.yml | 0
.../0.1.0/data_stream/test_logs}/manifest.yml | 0
.../test_metrics/fields/ecs.yml | 0
.../test_metrics/fields/fields.yml | 0
.../test_metrics/manifest.yml | 0
.../elasticsearch/ilm_policy/all_assets.json | 0
.../elasticsearch/ingest_pipeline/default.yml | 0
.../ingest_pipeline/pipeline1.yml | 0
.../test_logs/fields/ecs.yml | 0
.../test_logs/fields/fields.yml | 0
.../test_logs/manifest.yml | 0
.../test_logs2/fields/ecs.yml | 0
.../test_logs2/fields/fields.yml | 0
.../test_logs2/manifest.yml | 0
.../test_metrics/fields/ecs.yml | 0
.../test_metrics/fields/fields.yml | 0
.../test_metrics/manifest.yml | 0
.../elasticsearch/ilm_policy/all_assets.json | 0
.../elasticsearch/ingest_pipeline/default.yml | 0
.../test_logs/fields/ecs.yml | 0
.../test_logs/fields/fields.yml | 0
.../0.1.0/data_stream}/test_logs/manifest.yml | 0
.../test_metrics/fields/ecs.yml | 0
.../test_metrics/fields/fields.yml | 0
.../test_metrics/manifest.yml | 0
.../datastreams/0.1.0/manifest.yml | 7 +-
.../elasticsearch/ilm_policy/all_assets.json | 0
.../elasticsearch/ingest_pipeline/default.yml | 0
.../test_logs/fields/ecs.yml | 0
.../test_logs/fields/fields.yml | 0
.../test_logs/manifest.yml | 0
.../test_metrics/fields/ecs.yml | 0
.../test_metrics/fields/fields.yml | 0
.../test_metrics/manifest.yml | 0
.../datastreams/0.2.0/manifest.yml | 5 --
.../test/fields/fields.yml | 0
.../data_stream}/test/manifest.yml | 0
.../multiple_versions/0.1.0/manifest.yml | 9 +--
.../test/fields/fields.yml | 0
.../data_stream}/test/manifest.yml | 0
.../multiple_versions/0.2.0/manifest.yml | 9 +--
.../test/fields/fields.yml | 0
.../0.3.0/data_stream}/test/manifest.yml | 0
.../multiple_versions/0.3.0/manifest.yml | 7 +-
.../test/fields/fields.yml | 0
.../0.1.0/data_stream/test}/manifest.yml | 2 +-
.../test/fields/fields.yml | 0
.../test/manifest.yml | 0
.../apis/index.js | 2 +-
.../apps/endpoint/index.ts | 2 +-
.../apis/index.ts | 2 +-
84 files changed, 254 insertions(+), 268 deletions(-)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_logs/elasticsearch/ilm_policy/all_assets.json (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_logs/elasticsearch/ingest_pipeline/default.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_logs/elasticsearch/ingest_pipeline/pipeline2.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_logs/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_logs/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{multiple_versions/0.1.0/dataset/test => all_assets/0.1.0/data_stream/test_logs}/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_metrics/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_metrics/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/{dataset => data_stream}/test_metrics/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs/elasticsearch/ilm_policy/all_assets.json (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs/elasticsearch/ingest_pipeline/default.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs2/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs2/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_logs2/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_metrics/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_metrics/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/{dataset => data_stream}/test_metrics/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/{dataset => data_stream}/test_logs/elasticsearch/ilm_policy/all_assets.json (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/{dataset => data_stream}/test_logs/elasticsearch/ingest_pipeline/default.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/{dataset => data_stream}/test_logs/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/{dataset => data_stream}/test_logs/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{all_assets/0.1.0/dataset => datastreams/0.1.0/data_stream}/test_logs/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/{dataset => data_stream}/test_metrics/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/{dataset => data_stream}/test_metrics/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/{dataset => data_stream}/test_metrics/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/{dataset => data_stream}/test_logs/elasticsearch/ilm_policy/all_assets.json (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/{dataset => data_stream}/test_logs/elasticsearch/ingest_pipeline/default.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/{dataset => data_stream}/test_logs/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/{dataset => data_stream}/test_logs/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/{dataset => data_stream}/test_logs/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/{dataset => data_stream}/test_metrics/fields/ecs.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/{dataset => data_stream}/test_metrics/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/datastreams/0.2.0/{dataset => data_stream}/test_metrics/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.1.0/{dataset => data_stream}/test/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/{0.2.0/dataset => 0.1.0/data_stream}/test/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.2.0/{dataset => data_stream}/test/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/{0.3.0/dataset => 0.2.0/data_stream}/test/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/multiple_versions/0.3.0/{dataset => data_stream}/test/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{overrides/0.1.0/dataset => multiple_versions/0.3.0/data_stream}/test/manifest.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/{dataset => data_stream}/test/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/{datastreams/0.1.0/dataset/test_logs => overrides/0.1.0/data_stream/test}/manifest.yml (77%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/prerelease/0.1.0-dev.0+abc/{dataset => data_stream}/test/fields/fields.yml (100%)
rename x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/prerelease/0.1.0-dev.0+abc/{dataset => data_stream}/test/manifest.yml (100%)
diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
index 28a88aa2be6055..a780ae55997938 100644
--- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
+++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
@@ -1543,7 +1543,7 @@
}
},
"format_version": "1.0.0",
- "datasets": [
+ "data_streams": [
{
"title": "CoreDNS logs",
"name": "log",
@@ -1764,7 +1764,7 @@
]
}
},
- "datasets": [
+ "data_streams": [
{
"id": "endpoint",
"title": "Endpoint Events",
@@ -3961,7 +3961,7 @@
"format_version": {
"type": "string"
},
- "datasets": {
+ "data_streams": {
"type": "array",
"items": {
"type": "object",
diff --git a/x-pack/plugins/ingest_manager/common/services/limited_package.ts b/x-pack/plugins/ingest_manager/common/services/limited_package.ts
index 21d1dbd1556b7c..8d2a251ae015ee 100644
--- a/x-pack/plugins/ingest_manager/common/services/limited_package.ts
+++ b/x-pack/plugins/ingest_manager/common/services/limited_package.ts
@@ -7,7 +7,7 @@ import { PackageInfo, AgentPolicy, PackagePolicy } from '../types';
// Assume packages only ever include 1 config template for now
export const isPackageLimited = (packageInfo: PackageInfo): boolean => {
- return packageInfo.config_templates?.[0]?.multiple === false;
+ return packageInfo.policy_templates?.[0]?.multiple === false;
};
export const doesAgentPolicyAlreadyIncludePackage = (
diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts
index 6c3559d7cc5a03..a62fcddd16e0f0 100644
--- a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts
+++ b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts
@@ -34,14 +34,14 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
describe('packageToPackagePolicyInputs', () => {
it('returns empty array for packages with no config templates', () => {
expect(packageToPackagePolicyInputs(mockPackage)).toEqual([]);
- expect(packageToPackagePolicyInputs({ ...mockPackage, config_templates: [] })).toEqual([]);
+ expect(packageToPackagePolicyInputs({ ...mockPackage, policy_templates: [] })).toEqual([]);
});
it('returns empty array for packages with a config template but no inputs', () => {
expect(
packageToPackagePolicyInputs(({
...mockPackage,
- config_templates: [{ inputs: [] }],
+ policy_templates: [{ inputs: [] }],
} as unknown) as PackageInfo)
).toEqual([]);
});
@@ -50,13 +50,13 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
expect(
packageToPackagePolicyInputs(({
...mockPackage,
- config_templates: [{ inputs: [{ type: 'foo' }] }],
+ policy_templates: [{ inputs: [{ type: 'foo' }] }],
} as unknown) as PackageInfo)
).toEqual([{ type: 'foo', enabled: true, streams: [] }]);
expect(
packageToPackagePolicyInputs(({
...mockPackage,
- config_templates: [{ inputs: [{ type: 'foo' }, { type: 'bar' }] }],
+ policy_templates: [{ inputs: [{ type: 'foo' }, { type: 'bar' }] }],
} as unknown) as PackageInfo)
).toEqual([
{ type: 'foo', enabled: true, streams: [] },
@@ -68,12 +68,12 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
expect(
packageToPackagePolicyInputs(({
...mockPackage,
- datasets: [
- { type: 'logs', name: 'foo', streams: [{ input: 'foo' }] },
- { type: 'logs', name: 'bar', streams: [{ input: 'bar' }] },
- { type: 'logs', name: 'bar2', streams: [{ input: 'bar' }] },
+ data_streams: [
+ { type: 'logs', dataset: 'foo', streams: [{ input: 'foo' }] },
+ { type: 'logs', dataset: 'bar', streams: [{ input: 'bar' }] },
+ { type: 'logs', dataset: 'bar2', streams: [{ input: 'bar' }] },
],
- config_templates: [
+ policy_templates: [
{
inputs: [{ type: 'foo' }, { type: 'bar' }],
},
@@ -102,15 +102,15 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
expect(
packageToPackagePolicyInputs(({
...mockPackage,
- datasets: [
+ data_streams: [
{
type: 'logs',
- name: 'foo',
+ dataset: 'foo',
streams: [{ input: 'foo', vars: [{ default: 'foo-var-value', name: 'var-name' }] }],
},
{
type: 'logs',
- name: 'bar',
+ dataset: 'bar',
streams: [
{
input: 'bar',
@@ -120,7 +120,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
},
{
type: 'logs',
- name: 'bar2',
+ dataset: 'bar2',
streams: [
{
input: 'bar',
@@ -129,7 +129,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
],
},
],
- config_templates: [
+ policy_templates: [
{
inputs: [{ type: 'foo' }, { type: 'bar' }],
},
@@ -173,15 +173,15 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
expect(
packageToPackagePolicyInputs(({
...mockPackage,
- datasets: [
+ data_streams: [
{
type: 'logs',
- name: 'foo',
+ dataset: 'foo',
streams: [{ input: 'foo', vars: [{ default: 'foo-var-value', name: 'var-name' }] }],
},
{
type: 'logs',
- name: 'bar',
+ dataset: 'bar',
streams: [
{
input: 'bar',
@@ -191,7 +191,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
},
{
type: 'logs',
- name: 'bar2',
+ dataset: 'bar2',
streams: [
{
input: 'bar',
@@ -201,7 +201,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
},
{
type: 'logs',
- name: 'disabled',
+ dataset: 'disabled',
streams: [
{
input: 'with-disabled-streams',
@@ -212,7 +212,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
},
{
type: 'logs',
- name: 'disabled2',
+ dataset: 'disabled2',
streams: [
{
input: 'with-disabled-streams',
@@ -221,7 +221,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
],
},
],
- config_templates: [
+ policy_templates: [
{
inputs: [
{
@@ -372,13 +372,13 @@ describe('Ingest Manager - packageToPackagePolicy', () => {
});
});
it('returns package policy with inputs', () => {
- const mockPackageWithConfigTemplates = ({
+ const mockPackageWithPolicyTemplates = ({
...mockPackage,
- config_templates: [{ inputs: [{ type: 'foo' }] }],
+ policy_templates: [{ inputs: [{ type: 'foo' }] }],
} as unknown) as PackageInfo;
expect(
- packageToPackagePolicy(mockPackageWithConfigTemplates, '1', '2', 'default', 'pkgPolicy-1')
+ packageToPackagePolicy(mockPackageWithPolicyTemplates, '1', '2', 'default', 'pkgPolicy-1')
).toEqual({
policy_id: '1',
namespace: 'default',
diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.ts b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.ts
index eab2e8ac2d745a..822747916ebc5c 100644
--- a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.ts
+++ b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.ts
@@ -5,7 +5,7 @@
*/
import {
PackageInfo,
- RegistryConfigTemplate,
+ RegistryPolicyTemplate,
RegistryVarsEntry,
RegistryStream,
PackagePolicy,
@@ -22,14 +22,14 @@ const getStreamsForInputType = (
): Array => {
const streams: Array = [];
- (packageInfo.datasets || []).forEach((dataset) => {
- (dataset.streams || []).forEach((stream) => {
+ (packageInfo.data_streams || []).forEach((dataStream) => {
+ (dataStream.streams || []).forEach((stream) => {
if (stream.input === inputType) {
streams.push({
...stream,
data_stream: {
- type: dataset.type,
- dataset: dataset.name,
+ type: dataStream.type,
+ dataset: dataStream.dataset,
},
});
}
@@ -46,9 +46,9 @@ export const packageToPackagePolicyInputs = (packageInfo: PackageInfo): PackageP
const inputs: PackagePolicy['inputs'] = [];
// Assume package will only ever ship one package policy template for now
- const packagePolicyTemplate: RegistryConfigTemplate | null =
- packageInfo.config_templates && packageInfo.config_templates[0]
- ? packageInfo.config_templates[0]
+ const packagePolicyTemplate: RegistryPolicyTemplate | null =
+ packageInfo.policy_templates && packageInfo.policy_templates[0]
+ ? packageInfo.policy_templates[0]
: null;
// Create package policy input property
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 8bc5d9f7210b25..d2d1f22dda3a06 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -67,8 +67,8 @@ export interface RegistryPackage {
assets?: string[];
internal?: boolean;
format_version: string;
- datasets?: Dataset[];
- config_templates?: RegistryConfigTemplate[];
+ data_streams?: RegistryDataStream[];
+ policy_templates?: RegistryPolicyTemplate[];
download: string;
path: string;
}
@@ -80,7 +80,7 @@ interface RegistryImage {
size?: string;
type?: string;
}
-export interface RegistryConfigTemplate {
+export interface RegistryPolicyTemplate {
name: string;
title: string;
description: string;
@@ -127,8 +127,8 @@ export type RegistrySearchResult = Pick<
| 'internal'
| 'download'
| 'path'
- | 'datasets'
- | 'config_templates'
+ | 'data_streams'
+ | 'policy_templates'
>;
export type ScreenshotItem = RegistryImage;
@@ -174,9 +174,9 @@ export type ElasticsearchAssetTypeToParts = Record<
ElasticsearchAssetParts[]
>;
-export interface Dataset {
+export interface RegistryDataStream {
type: string;
- name: string;
+ dataset: string;
title: string;
release: string;
streams?: RegistryStream[];
diff --git a/x-pack/plugins/ingest_manager/dev_docs/epm.md b/x-pack/plugins/ingest_manager/dev_docs/epm.md
index 20209d09e6cc21..a066b6deb3bc84 100644
--- a/x-pack/plugins/ingest_manager/dev_docs/epm.md
+++ b/x-pack/plugins/ingest_manager/dev_docs/epm.md
@@ -26,5 +26,5 @@ When a package is installed or upgraded, certain Kibana and Elasticsearch assets
### Generation
- Index templates are generated from `YAML` files contained in the package.
-- There is one index template per dataset.
-- For the generation of an index template, all `yml` files contained in the package subdirectory `dataset/DATASET_NAME/fields/` are used.
+- There is one index template per data stream.
+- For the generation of an index template, all `yml` files contained in the package subdirectory `data_stream/DATASET_NAME/fields/` are used.
diff --git a/x-pack/plugins/ingest_manager/dev_docs/indexing_strategy.md b/x-pack/plugins/ingest_manager/dev_docs/indexing_strategy.md
index fd7edcb7fcca0b..42a0bbc2188699 100644
--- a/x-pack/plugins/ingest_manager/dev_docs/indexing_strategy.md
+++ b/x-pack/plugins/ingest_manager/dev_docs/indexing_strategy.md
@@ -6,48 +6,48 @@ Overall documentation of Ingest Management is now maintained in the `elastic/sta
Ingest Management enforces an indexing strategy to allow the system to automatically detect indices and run queries on it. In short the indexing strategy looks as following:
```
-{dataset.type}-{dataset.name}-{dataset.namespace}
+{data_stream.type}-{data_stream.dataset}-{data_stream.namespace}
```
-The `{dataset.type}` can be `logs` or `metrics`. The `{dataset.namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `dataset` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, namespace and dataset are part of each event and are constant keywords. If there is a dataset or a namespace with a `-` inside, it is recommended to replace it either by a `.` or a `_`.
+The `{data_stream.type}` can be `logs` or `metrics`. The `{data_stream.namespace}` is the part where the user can use free form. The only two requirement are that it has only characters allowed in an Elasticsearch index name and does NOT contain a `-`. The `data_stream` is defined by the data that is indexed. The same requirements as for the namespace apply. It is expected that the fields for type, dataset, and namespace are part of each event and are constant keywords. If there is a dataset or a namespace with a `-` inside, it is recommended to replace it either by a `.` or a `_`.
-Note: More `{dataset.type}`s might be added in the future like `traces`.
+Note: More `{data_stream.type}`s might be added in the future like `traces`.
This indexing strategy has a few advantages:
-* Each index contains only the fields which are relevant for the dataset. This leads to more dense indices and better field completion.
-* ILM policies can be applied per namespace per dataset.
-* Rollups can be specified per namespace per dataset.
-* Having the namespace user configurable makes setting security permissions possible.
-* Having a global metrics and logs template, allows to create new indices on demand which still follow the convention. This is common in the case of k8s as an example.
-* Constant keywords allow to narrow down the indices we need to access for querying very efficiently. This is especially relevant in environments which a large number of indices or with indices on slower nodes.
+- Each index contains only the fields which are relevant for the datta stream. This leads to more dense indices and better field completion.
+- ILM policies can be applied per namespace per data stream.
+- Rollups can be specified per namespace per data stream.
+- Having the namespace user configurable makes setting security permissions possible.
+- Having a global metrics and logs template, allows to create new indices on demand which still follow the convention. This is common in the case of k8s as an example.
+- Constant keywords allow to narrow down the indices we need to access for querying very efficiently. This is especially relevant in environments which a large number of indices or with indices on slower nodes.
Overall it creates smaller indices in size, makes querying more efficient and allows users to define their own naming parts in namespace and still benefiting from all features that can be built on top of the indexing startegy.
## Ingest Pipeline
-The ingest pipelines for a specific dataset will have the following naming scheme:
+The ingest pipelines for a specific data stream will have the following naming scheme:
```
-{dataset.type}-{dataset.name}-{package.version}
+{data_stream.type}-{data_stream.dataset}-{package.version}
```
-As an example, the ingest pipeline for the Nginx access logs is called `logs-nginx.access-3.4.1`. The same ingest pipeline is used for all namespaces. It is possible that a dataset has multiple ingest pipelines in which case a suffix is added to the name.
+As an example, the ingest pipeline for the Nginx access logs is called `logs-nginx.access-3.4.1`. The same ingest pipeline is used for all namespaces. It is possible that a data stream has multiple ingest pipelines in which case a suffix is added to the name.
The version is included in each pipeline to allow upgrades. The pipeline itself is listed in the index template and is automatically applied at ingest time.
## Templates & ILM Policies
-To make the above strategy possible, alias templates are required. For each type there is a basic alias template with a default ILM policy. These default templates apply to all indices which follow the indexing strategy and do not have a more specific dataset alias template.
+To make the above strategy possible, alias templates are required. For each type there is a basic alias template with a default ILM policy. These default templates apply to all indices which follow the indexing strategy and do not have a more specific data stream alias template.
The `metrics` and `logs` alias template contain all the basic fields from ECS.
Each type template contains an ILM policy. Modifying this default ILM policy will affect all data covered by the default templates.
-The templates for a dataset are called as following:
+The templates for a data stream are called as following:
```
-{dataset.type}-{dataset.name}
+{data_stream.type}-{data_stream.dataset}
```
The pattern used inside the index template is `{type}-{dataset}-*` to match all namespaces.
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts
index aae750cb674991..d621db615f2bd6 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts
@@ -7,7 +7,7 @@ import {
PackageInfo,
InstallationStatus,
NewPackagePolicy,
- RegistryConfigTemplate,
+ RegistryPolicyTemplate,
} from '../../../../types';
import { validatePackagePolicy, validationHasErrors } from './validate_package_policy';
@@ -32,9 +32,9 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
},
},
status: InstallationStatus.notInstalled,
- datasets: [
+ data_streams: [
{
- name: 'foo',
+ dataset: 'foo',
streams: [
{
input: 'foo',
@@ -44,7 +44,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
],
},
{
- name: 'bar',
+ dataset: 'bar',
streams: [
{
input: 'bar',
@@ -59,7 +59,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
],
},
{
- name: 'bar2',
+ dataset: 'bar2',
streams: [
{
input: 'bar',
@@ -69,7 +69,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
],
},
{
- name: 'disabled',
+ dataset: 'disabled',
streams: [
{
input: 'with-disabled-streams',
@@ -80,7 +80,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
],
},
{
- name: 'disabled2',
+ dataset: 'disabled2',
streams: [
{
input: 'with-disabled-streams',
@@ -90,7 +90,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
],
},
],
- config_templates: [
+ policy_templates: [
{
name: 'pkgPolicy1',
title: 'Package policy 1',
@@ -465,7 +465,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
expect(
validatePackagePolicy(validPackagePolicy, {
...mockPackage,
- config_templates: undefined,
+ policy_templates: undefined,
})
).toEqual({
name: null,
@@ -476,7 +476,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
expect(
validatePackagePolicy(validPackagePolicy, {
...mockPackage,
- config_templates: [],
+ policy_templates: [],
})
).toEqual({
name: null,
@@ -490,7 +490,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
expect(
validatePackagePolicy(validPackagePolicy, {
...mockPackage,
- config_templates: [{} as RegistryConfigTemplate],
+ policy_templates: [{} as RegistryPolicyTemplate],
})
).toEqual({
name: null,
@@ -501,7 +501,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => {
expect(
validatePackagePolicy(validPackagePolicy, {
...mockPackage,
- config_templates: [({ inputs: [] } as unknown) as RegistryConfigTemplate],
+ policy_templates: [({ inputs: [] } as unknown) as RegistryPolicyTemplate],
})
).toEqual({
name: null,
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts
index 03060c5dcb20e0..04cd21884e8f28 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts
@@ -65,11 +65,11 @@ export const validatePackagePolicy = (
}
if (
- !packageInfo.config_templates ||
- packageInfo.config_templates.length === 0 ||
- !packageInfo.config_templates[0] ||
- !packageInfo.config_templates[0].inputs ||
- packageInfo.config_templates[0].inputs.length === 0
+ !packageInfo.policy_templates ||
+ packageInfo.policy_templates.length === 0 ||
+ !packageInfo.policy_templates[0] ||
+ !packageInfo.policy_templates[0].inputs ||
+ packageInfo.policy_templates[0].inputs.length === 0
) {
validationResults.inputs = null;
return validationResults;
@@ -78,16 +78,16 @@ export const validatePackagePolicy = (
const registryInputsByType: Record<
string,
RegistryInput
- > = packageInfo.config_templates[0].inputs.reduce((inputs, registryInput) => {
+ > = packageInfo.policy_templates[0].inputs.reduce((inputs, registryInput) => {
inputs[registryInput.type] = registryInput;
return inputs;
}, {} as Record);
const registryStreamsByDataset: Record = (
- packageInfo.datasets || []
- ).reduce((datasets, registryDataset) => {
- datasets[registryDataset.name] = registryDataset.streams || [];
- return datasets;
+ packageInfo.data_streams || []
+ ).reduce((dataStreams, registryDataStream) => {
+ dataStreams[registryDataStream.dataset] = registryDataStream.streams || [];
+ return dataStreams;
}, {} as Record);
// Validate each package policy input with either its own config fields or streams
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_configure_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_configure_package.tsx
index b77153daee2fc5..d3d5e60c34e584 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_configure_package.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_configure_package.tsx
@@ -17,13 +17,13 @@ const findStreamsForInputType = (
): Array => {
const streams: Array = [];
- (packageInfo.datasets || []).forEach((dataset) => {
- (dataset.streams || []).forEach((stream) => {
+ (packageInfo.data_streams || []).forEach((dataStream) => {
+ (dataStream.streams || []).forEach((stream) => {
if (stream.input === inputType) {
streams.push({
...stream,
data_stream: {
- dataset: dataset.name,
+ dataset: dataStream.dataset,
},
});
}
@@ -53,14 +53,14 @@ export const StepConfigurePackagePolicy: React.FunctionComponent<{
// Configure inputs (and their streams)
// Assume packages only export one config template for now
const renderConfigureInputs = () =>
- packageInfo.config_templates &&
- packageInfo.config_templates[0] &&
- packageInfo.config_templates[0].inputs &&
- packageInfo.config_templates[0].inputs.length ? (
+ packageInfo.policy_templates &&
+ packageInfo.policy_templates[0] &&
+ packageInfo.policy_templates[0].inputs &&
+ packageInfo.policy_templates[0].inputs.length ? (
<>
- {packageInfo.config_templates[0].inputs.map((packageInput) => {
+ {packageInfo.policy_templates[0].inputs.map((packageInput) => {
const packagePolicyInput = packagePolicy.inputs.find(
(input) => input.type === packageInput.type
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
index 71a44089b8bf77..e825448f359d63 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts
@@ -89,7 +89,7 @@ export {
RegistryVarsEntry,
RegistryInput,
RegistryStream,
- RegistryConfigTemplate,
+ RegistryPolicyTemplate,
PackageList,
PackageListItem,
PackagesGroupedByStatus,
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/index.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/index.test.ts
index bdd8883ea29c24..78aa17da5030c6 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/index.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/index.test.ts
@@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Dataset } from '../../../types';
-import { getDatasetAssetBaseName } from './index';
+import { RegistryDataStream } from '../../../types';
+import { getRegistryDataStreamAssetBaseName } from './index';
test('getBaseName', () => {
- const dataset: Dataset = {
- name: 'nginx.access',
+ const dataStream: RegistryDataStream = {
+ dataset: 'nginx.access',
title: 'Nginx Acess Logs',
release: 'beta',
type: 'logs',
@@ -17,6 +17,6 @@ test('getBaseName', () => {
package: 'nginx',
path: 'access',
};
- const name = getDatasetAssetBaseName(dataset);
+ const name = getRegistryDataStreamAssetBaseName(dataStream);
expect(name).toStrictEqual('logs-nginx.access');
});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/index.ts
index 0cb09ba054bf10..17cd28cc8a0811 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/index.ts
@@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Dataset } from '../../../types';
+import { RegistryDataStream } from '../../../types';
/**
* Creates the base name for Elasticsearch assets in the form of
- * {type}-{id}
+ * {type}-{dataset}
*/
-export function getDatasetAssetBaseName(dataset: Dataset): string {
- return `${dataset.type}-${dataset.name}`;
+export function getRegistryDataStreamAssetBaseName(dataStream: RegistryDataStream): string {
+ return `${dataStream.type}-${dataStream.dataset}`;
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/ingest_pipelines.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/ingest_pipelines.test.ts
index 36a19c512a8b48..378dd271779b4e 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/ingest_pipelines.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/ingest_pipelines.test.ts
@@ -7,7 +7,7 @@
import { readFileSync } from 'fs';
import path from 'path';
import { rewriteIngestPipeline, getPipelineNameForInstallation } from './install';
-import { Dataset } from '../../../../types';
+import { RegistryDataStream } from '../../../../types';
test('a json-format pipeline with pipeline references is correctly rewritten', () => {
const inputStandard = readFileSync(
@@ -106,8 +106,8 @@ test('a yml-format pipeline with no pipeline references stays unchanged', () =>
});
test('getPipelineNameForInstallation gets correct name', () => {
- const dataset: Dataset = {
- name: 'coredns.log',
+ const dataStream: RegistryDataStream = {
+ dataset: 'coredns.log',
title: 'CoreDNS logs',
release: 'ga',
type: 'logs',
@@ -118,19 +118,19 @@ test('getPipelineNameForInstallation gets correct name', () => {
const packageVersion = '1.0.1';
const pipelineRefName = 'pipeline-json';
const pipelineEntryNameForInstallation = getPipelineNameForInstallation({
- pipelineName: dataset.ingest_pipeline,
- dataset,
+ pipelineName: dataStream.ingest_pipeline,
+ dataStream,
packageVersion,
});
const pipelineRefNameForInstallation = getPipelineNameForInstallation({
pipelineName: pipelineRefName,
- dataset,
+ dataStream,
packageVersion,
});
expect(pipelineEntryNameForInstallation).toBe(
- `${dataset.type}-${dataset.name}-${packageVersion}`
+ `${dataStream.type}-${dataStream.dataset}-${packageVersion}`
);
expect(pipelineRefNameForInstallation).toBe(
- `${dataset.type}-${dataset.name}-${packageVersion}-${pipelineRefName}`
+ `${dataStream.type}-${dataStream.dataset}-${packageVersion}-${pipelineRefName}`
);
});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts
index 878c6ea8f28047..6088bcb71f8789 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts
@@ -7,7 +7,7 @@
import { SavedObjectsClientContract } from 'src/core/server';
import {
EsAssetReference,
- Dataset,
+ RegistryDataStream,
ElasticsearchAssetType,
RegistryPackage,
} from '../../../../types';
@@ -30,17 +30,19 @@ export const installPipelines = async (
// unlike other ES assets, pipeline names are versioned so after a template is updated
// it can be created pointing to the new template, without removing the old one and effecting data
// so do not remove the currently installed pipelines here
- const datasets = registryPackage.datasets;
- if (!datasets?.length) return [];
+ const dataStreams = registryPackage.data_streams;
+ if (!dataStreams?.length) return [];
const pipelinePaths = paths.filter((path) => isPipeline(path));
// get and save pipeline refs before installing pipelines
- const pipelineRefs = datasets.reduce((acc, dataset) => {
- const filteredPaths = pipelinePaths.filter((path) => isDatasetPipeline(path, dataset.path));
+ const pipelineRefs = dataStreams.reduce((acc, dataStream) => {
+ const filteredPaths = pipelinePaths.filter((path) =>
+ isDataStreamPipeline(path, dataStream.path)
+ );
const pipelineObjectRefs = filteredPaths.map((path) => {
const { name } = getNameAndExtension(path);
const nameForInstallation = getPipelineNameForInstallation({
pipelineName: name,
- dataset,
+ dataStream,
packageVersion: registryPackage.version,
});
return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline };
@@ -49,11 +51,11 @@ export const installPipelines = async (
return acc;
}, []);
await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, pipelineRefs);
- const pipelines = datasets.reduce>>((acc, dataset) => {
- if (dataset.ingest_pipeline) {
+ const pipelines = dataStreams.reduce>>((acc, dataStream) => {
+ if (dataStream.ingest_pipeline) {
acc.push(
- installPipelinesForDataset({
- dataset,
+ installPipelinesForDataStream({
+ dataStream,
callCluster,
paths: pipelinePaths,
pkgVersion: registryPackage.version,
@@ -86,18 +88,18 @@ export function rewriteIngestPipeline(
return pipeline;
}
-export async function installPipelinesForDataset({
+export async function installPipelinesForDataStream({
callCluster,
pkgVersion,
paths,
- dataset,
+ dataStream,
}: {
callCluster: CallESAsCurrentUser;
pkgVersion: string;
paths: string[];
- dataset: Dataset;
+ dataStream: RegistryDataStream;
}): Promise {
- const pipelinePaths = paths.filter((path) => isDatasetPipeline(path, dataset.path));
+ const pipelinePaths = paths.filter((path) => isDataStreamPipeline(path, dataStream.path));
let pipelines: any[] = [];
const substitutions: RewriteSubstitution[] = [];
@@ -105,7 +107,7 @@ export async function installPipelinesForDataset({
const { name, extension } = getNameAndExtension(path);
const nameForInstallation = getPipelineNameForInstallation({
pipelineName: name,
- dataset,
+ dataStream,
packageVersion: pkgVersion,
});
const content = Registry.getAsset(path).toString('utf-8');
@@ -175,13 +177,13 @@ async function installPipeline({
const isDirectory = ({ path }: Registry.ArchiveEntry) => path.endsWith('/');
-const isDatasetPipeline = (path: string, datasetName: string) => {
+const isDataStreamPipeline = (path: string, dataStreamDataset: string) => {
const pathParts = Registry.pathParts(path);
return (
!isDirectory({ path }) &&
pathParts.type === ElasticsearchAssetType.ingestPipeline &&
pathParts.dataset !== undefined &&
- datasetName === pathParts.dataset
+ dataStreamDataset === pathParts.dataset
);
};
const isPipeline = (path: string) => {
@@ -206,15 +208,15 @@ const getNameAndExtension = (
export const getPipelineNameForInstallation = ({
pipelineName,
- dataset,
+ dataStream,
packageVersion,
}: {
pipelineName: string;
- dataset: Dataset;
+ dataStream: RegistryDataStream;
packageVersion: string;
}): string => {
- const isPipelineEntry = pipelineName === dataset.ingest_pipeline;
+ const isPipelineEntry = pipelineName === dataStream.ingest_pipeline;
const suffix = isPipelineEntry ? '' : `-${pipelineName}`;
// if this is the pipeline entry, don't add a suffix
- return `${dataset.type}-${dataset.name}-${packageVersion}${suffix}`;
+ return `${dataStream.type}-${dataStream.dataset}-${packageVersion}${suffix}`;
};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
index f4e8c3bfd99d3f..8f80feb268910e 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
@@ -7,7 +7,7 @@
import Boom from 'boom';
import { SavedObjectsClientContract } from 'src/core/server';
import {
- Dataset,
+ RegistryDataStream,
RegistryPackage,
ElasticsearchAssetType,
TemplateRef,
@@ -38,29 +38,32 @@ export const installTemplates = async (
registryPackage.name,
ElasticsearchAssetType.indexTemplate
);
- // build templates per dataset from yml files
- const datasets = registryPackage.datasets;
- if (!datasets) return [];
+ // build templates per data stream from yml files
+ const dataStreams = registryPackage.data_streams;
+ if (!dataStreams) return [];
// get template refs to save
- const installedTemplateRefs = datasets.map((dataset) => ({
- id: generateTemplateName(dataset),
+ const installedTemplateRefs = dataStreams.map((dataStream) => ({
+ id: generateTemplateName(dataStream),
type: ElasticsearchAssetType.indexTemplate,
}));
// add package installation's references to index templates
await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, installedTemplateRefs);
- if (datasets) {
- const installTemplatePromises = datasets.reduce>>((acc, dataset) => {
- acc.push(
- installTemplateForDataset({
- pkg: registryPackage,
- callCluster,
- dataset,
- })
- );
- return acc;
- }, []);
+ if (dataStreams) {
+ const installTemplatePromises = dataStreams.reduce>>(
+ (acc, dataStream) => {
+ acc.push(
+ installTemplateForDataStream({
+ pkg: registryPackage,
+ callCluster,
+ dataStream,
+ })
+ );
+ return acc;
+ },
+ []
+ );
const res = await Promise.all(installTemplatePromises);
const installedTemplates = res.flat();
@@ -158,25 +161,25 @@ const isComponentTemplate = (path: string) => {
};
/**
- * installTemplatesForDataset installs one template for each dataset
+ * installTemplateForDataStream installs one template for each data stream
*
- * The template is currently loaded with the pkgey-package-dataset
+ * The template is currently loaded with the pkgkey-package-data_stream
*/
-export async function installTemplateForDataset({
+export async function installTemplateForDataStream({
pkg,
callCluster,
- dataset,
+ dataStream,
}: {
pkg: RegistryPackage;
callCluster: CallESAsCurrentUser;
- dataset: Dataset;
+ dataStream: RegistryDataStream;
}): Promise {
- const fields = await loadFieldsFromYaml(pkg, dataset.path);
+ const fields = await loadFieldsFromYaml(pkg, dataStream.path);
return installTemplate({
callCluster,
fields,
- dataset,
+ dataStream,
packageVersion: pkg.version,
packageName: pkg.name,
});
@@ -237,7 +240,7 @@ function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch |
return { settingsTemplate, mappingsTemplate };
}
-async function installDatasetComponentTemplates(
+async function installDataStreamComponentTemplates(
templateName: string,
registryElasticsearch: RegistryElasticsearch | undefined,
callCluster: CallESAsCurrentUser
@@ -277,35 +280,35 @@ async function installDatasetComponentTemplates(
export async function installTemplate({
callCluster,
fields,
- dataset,
+ dataStream,
packageVersion,
packageName,
}: {
callCluster: CallESAsCurrentUser;
fields: Field[];
- dataset: Dataset;
+ dataStream: RegistryDataStream;
packageVersion: string;
packageName: string;
}): Promise {
const mappings = generateMappings(processFields(fields));
- const templateName = generateTemplateName(dataset);
+ const templateName = generateTemplateName(dataStream);
let pipelineName;
- if (dataset.ingest_pipeline) {
+ if (dataStream.ingest_pipeline) {
pipelineName = getPipelineNameForInstallation({
- pipelineName: dataset.ingest_pipeline,
- dataset,
+ pipelineName: dataStream.ingest_pipeline,
+ dataStream,
packageVersion,
});
}
- const composedOfTemplates = await installDatasetComponentTemplates(
+ const composedOfTemplates = await installDataStreamComponentTemplates(
templateName,
- dataset.elasticsearch,
+ dataStream.elasticsearch,
callCluster
);
const template = getTemplate({
- type: dataset.type,
+ type: dataStream.type,
templateName,
mappings,
pipelineName,
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
index 71e49acf1766fc..00c2e873ba1295 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
@@ -6,13 +6,13 @@
import { Field, Fields } from '../../fields/field';
import {
- Dataset,
+ RegistryDataStream,
CallESAsCurrentUser,
TemplateRef,
IndexTemplate,
IndexTemplateMappings,
} from '../../../../types';
-import { getDatasetAssetBaseName } from '../index';
+import { getRegistryDataStreamAssetBaseName } from '../index';
interface Properties {
[key: string]: any;
@@ -222,22 +222,24 @@ function getDefaultProperties(field: Field): Properties {
/**
* Generates the template name out of the given information
*/
-export function generateTemplateName(dataset: Dataset): string {
- return getDatasetAssetBaseName(dataset);
+export function generateTemplateName(dataStream: RegistryDataStream): string {
+ return getRegistryDataStreamAssetBaseName(dataStream);
}
/**
- * Returns a map of the dataset path fields to elasticsearch index pattern.
- * @param datasets an array of Dataset objects
+ * Returns a map of the data stream path fields to elasticsearch index pattern.
+ * @param dataStreams an array of RegistryDataStream objects
*/
-export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record {
- if (!datasets) {
+export function generateESIndexPatterns(
+ dataStreams: RegistryDataStream[] | undefined
+): Record {
+ if (!dataStreams) {
return {};
}
const patterns: Record = {};
- for (const dataset of datasets) {
- patterns[dataset.path] = generateTemplateName(dataset) + '-*';
+ for (const dataStream of dataStreams) {
+ patterns[dataStream.path] = generateTemplateName(dataStream) + '-*';
}
return patterns;
}
@@ -389,7 +391,7 @@ const updateExistingIndex = async ({
}) => {
const { settings, mappings } = indexTemplate.template;
- // for now, remove from object so as not to update stream or dataset properties of the index until type and name
+ // for now, remove from object so as not to update stream or data stream properties of the index until type and name
// are added in https://github.com/elastic/kibana/issues/66551. namespace value we will continue
// to skip updating and assume the value in the index mapping is correct
delete mappings.properties.stream;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
index 7cb507d15679ed..768c6af1d89154 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
@@ -114,10 +114,10 @@ describe('test transform install', () => {
({
name: 'endpoint',
version: '0.16.0-dev.0',
- datasets: [
+ data_streams: [
{
type: 'metrics',
- name: 'endpoint.metadata',
+ dataset: 'endpoint.metadata',
title: 'Endpoint Metadata',
release: 'experimental',
package: 'endpoint',
@@ -131,7 +131,7 @@ describe('test transform install', () => {
},
{
type: 'metrics',
- name: 'endpoint.metadata_current',
+ dataset: 'endpoint.metadata_current',
title: 'Endpoint Metadata Current',
release: 'experimental',
package: 'endpoint',
@@ -146,7 +146,7 @@ describe('test transform install', () => {
],
} as unknown) as RegistryPackage,
[
- 'endpoint-0.16.0-dev.0/dataset/policy/elasticsearch/ingest_pipeline/default.json',
+ 'endpoint-0.16.0-dev.0/data_stream/policy/elasticsearch/ingest_pipeline/default.json',
'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata/default.json',
'endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json',
],
@@ -302,10 +302,10 @@ describe('test transform install', () => {
({
name: 'endpoint',
version: '0.16.0-dev.0',
- datasets: [
+ data_streams: [
{
type: 'metrics',
- name: 'endpoint.metadata_current',
+ dataset: 'endpoint.metadata_current',
title: 'Endpoint Metadata',
release: 'experimental',
package: 'endpoint',
@@ -404,10 +404,10 @@ describe('test transform install', () => {
({
name: 'endpoint',
version: '0.16.0-dev.0',
- datasets: [
+ data_streams: [
{
type: 'metrics',
- name: 'endpoint.metadata',
+ dataset: 'endpoint.metadata',
title: 'Endpoint Metadata',
release: 'experimental',
package: 'endpoint',
@@ -421,7 +421,7 @@ describe('test transform install', () => {
},
{
type: 'metrics',
- name: 'endpoint.metadata_current',
+ dataset: 'endpoint.metadata_current',
title: 'Endpoint Metadata Current',
release: 'experimental',
package: 'endpoint',
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
index 7fe3713e186ee0..bde542412f1237 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
@@ -122,8 +122,8 @@ export async function installIndexPatterns(
return;
}
- // get all dataset fields from all installed packages
- const fields = await getAllDatasetFieldsByType(installedPackagesInfo, indexPatternType);
+ // get all data stream fields from all installed packages
+ const fields = await getAllDataStreamFieldsByType(installedPackagesInfo, indexPatternType);
const kibanaIndexPattern = createIndexPattern(indexPatternType, fields);
// create or overwrite the index pattern
@@ -135,23 +135,27 @@ export async function installIndexPatterns(
}
// loops through all given packages and returns an array
-// of all fields from all datasets matching datasetType
-export const getAllDatasetFieldsByType = async (
+// of all fields from all data streams matching data stream type
+export const getAllDataStreamFieldsByType = async (
packages: RegistryPackage[],
- datasetType: IndexPatternType
+ dataStreamType: IndexPatternType
): Promise => {
- const datasetsPromises = packages.reduce>>((acc, pkg) => {
- if (pkg.datasets) {
- // filter out datasets by datasetType
- const matchingDatasets = pkg.datasets.filter((dataset) => dataset.type === datasetType);
- matchingDatasets.forEach((dataset) => acc.push(loadFieldsFromYaml(pkg, dataset.path)));
+ const dataStreamsPromises = packages.reduce>>((acc, pkg) => {
+ if (pkg.data_streams) {
+ // filter out data streams by data stream type
+ const matchingDataStreams = pkg.data_streams.filter(
+ (dataStream) => dataStream.type === dataStreamType
+ );
+ matchingDataStreams.forEach((dataStream) =>
+ acc.push(loadFieldsFromYaml(pkg, dataStream.path))
+ );
}
return acc;
}, []);
- // get all the datasets for each installed package into one array
- const allDatasetFields: Fields[] = await Promise.all(datasetsPromises);
- return allDatasetFields.flat();
+ // get all the data stream fields for each installed package into one array
+ const allDataStreamFields: Fields[] = await Promise.all(dataStreamsPromises);
+ return allDataStreamFields.flat();
};
// creates or updates index pattern
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
index 6d5ca036aeb130..78b42b03be8318 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
@@ -11,8 +11,8 @@ const tests = [
{
package: {
assets: [
- '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
- '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
+ '/package/coredns/1.0.1/data_stream/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
+ '/package/coredns/1.0.1/data_stream/log/elasticsearch/ingest-pipeline/pipeline-json.json',
],
path: '/package/coredns/1.0.1',
},
@@ -21,15 +21,15 @@ const tests = [
return true;
},
expected: [
- '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
- '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
+ '/package/coredns/1.0.1/data_stream/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
+ '/package/coredns/1.0.1/data_stream/log/elasticsearch/ingest-pipeline/pipeline-json.json',
],
},
{
package: {
assets: [
- '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
- '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
+ '/package/coredns-1.0.1/data_stream/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
+ '/package/coredns-1.0.1/data_stream/log/elasticsearch/ingest-pipeline/pipeline-json.json',
],
path: '/package/coredns/1.0.1',
},
@@ -43,8 +43,8 @@ const tests = [
{
package: {
assets: [
- '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
- '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
+ '/package/coredns-1.0.1/data_stream/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
+ '/package/coredns-1.0.1/data_stream/log/elasticsearch/ingest-pipeline/pipeline-json.json',
],
},
// Filter which does not exist
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
index 19a023eb2ad4c5..a8abc12917781d 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
@@ -9,9 +9,9 @@ import * as Registry from '../registry';
import { ensureCachedArchiveInfo } from '../registry';
// paths from RegistryPackage are routes to the assets on EPR
-// e.g. `/package/nginx/1.2.0/dataset/access/fields/fields.yml`
+// e.g. `/package/nginx/1.2.0/data_stream/access/fields/fields.yml`
// paths for ArchiveEntry are routes to the assets in the archive
-// e.g. `nginx-1.2.0/dataset/access/fields/fields.yml`
+// e.g. `nginx-1.2.0/data_stream/access/fields/fields.yml`
// RegistryPackage paths have a `/package/` prefix compared to ArchiveEntry paths
// and different package and version structure
const EPR_PATH_PREFIX = '/package';
@@ -37,7 +37,7 @@ export function getAssets(
// if dataset, filter for them
if (datasetName) {
- const comparePath = `${packageInfo.path}/dataset/${datasetName}/`;
+ const comparePath = `${packageInfo.path}/data_stream/${datasetName}/`;
if (!path.includes(comparePath)) {
continue;
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index d501b05d96c1cc..d7262ebb66b2e8 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -259,7 +259,7 @@ export async function installPackage({
const removable = !isRequiredPackage(pkgName);
const { internal = false } = registryPackageInfo;
- const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets);
+ const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.data_streams);
// add the package installation to the saved object.
// if some installation already exists, just update install info
@@ -304,7 +304,7 @@ export async function installPackage({
// currently only the base package has an ILM policy
// at some point ILM policies can be installed/modified
- // per dataset and we should then save them
+ // per data stream and we should then save them
await installILMPolicy(paths, callCluster);
// installs versionized pipelines without removing currently installed ones
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts
index b40638eefbae2f..2fd9175549026e 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.test.ts
@@ -41,11 +41,11 @@ const testPaths = [
},
},
{
- path: 'coredns-1.0.1/dataset/stats/fields/coredns.stats.yml',
+ path: 'coredns-1.0.1/data_stream/stats/fields/coredns.stats.yml',
assetParts: {
dataset: 'stats',
file: 'coredns.stats.yml',
- path: 'coredns-1.0.1/dataset/stats/fields/coredns.stats.yml',
+ path: 'coredns-1.0.1/data_stream/stats/fields/coredns.stats.yml',
pkgkey: 'coredns-1.0.1',
service: '',
type: 'fields',
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
index 96f75306413905..22f1b670b2cc4f 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
@@ -158,12 +158,12 @@ export function pathParts(path: string): AssetParts {
let [pkgkey, service, type, file] = path.split('/');
- // if it's a dataset
- if (service === 'dataset') {
+ // if it's a data stream
+ if (service === 'data_stream') {
// save the dataset name
dataset = type;
- // drop the `dataset/dataset-name` portion & re-parse
- [pkgkey, service, type, file] = path.replace(`dataset/${dataset}/`, '').split('/');
+ // drop the `data_stream/dataset-name` portion & re-parse
+ [pkgkey, service, type, file] = path.replace(`data_stream/${dataset}/`, '').split('/');
}
// This is to cover for the fields.yml files inside the "fields" directory
diff --git a/x-pack/plugins/ingest_manager/server/services/package_policy.test.ts b/x-pack/plugins/ingest_manager/server/services/package_policy.test.ts
index 0d89c529576327..6064e5bae06348 100644
--- a/x-pack/plugins/ingest_manager/server/services/package_policy.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/package_policy.test.ts
@@ -45,14 +45,14 @@ describe('Package policy service', () => {
it('should work with config variables from the stream', async () => {
const inputs = await packagePolicyService.assignPackageStream(
({
- datasets: [
+ data_streams: [
{
type: 'logs',
- name: 'package.dataset1',
+ dataset: 'package.dataset1',
streams: [{ input: 'log', template_path: 'some_template_path.yml' }],
},
],
- config_templates: [
+ policy_templates: [
{
inputs: [{ type: 'log' }],
},
@@ -64,7 +64,7 @@ describe('Package policy service', () => {
enabled: true,
streams: [
{
- id: 'dataset01',
+ id: 'datastream01',
data_stream: { dataset: 'package.dataset1', type: 'logs' },
enabled: true,
vars: {
@@ -84,7 +84,7 @@ describe('Package policy service', () => {
enabled: true,
streams: [
{
- id: 'dataset01',
+ id: 'datastream01',
data_stream: { dataset: 'package.dataset1', type: 'logs' },
enabled: true,
vars: {
@@ -106,14 +106,14 @@ describe('Package policy service', () => {
it('should work with config variables at the input level', async () => {
const inputs = await packagePolicyService.assignPackageStream(
({
- datasets: [
+ data_streams: [
{
- name: 'package.dataset1',
+ dataset: 'package.dataset1',
type: 'logs',
streams: [{ input: 'log', template_path: 'some_template_path.yml' }],
},
],
- config_templates: [
+ policy_templates: [
{
inputs: [{ type: 'log' }],
},
@@ -130,7 +130,7 @@ describe('Package policy service', () => {
},
streams: [
{
- id: 'dataset01',
+ id: 'datastream01',
data_stream: { dataset: 'package.dataset1', type: 'logs' },
enabled: true,
},
@@ -150,7 +150,7 @@ describe('Package policy service', () => {
},
streams: [
{
- id: 'dataset01',
+ id: 'datastream01',
data_stream: { dataset: 'package.dataset1', type: 'logs' },
enabled: true,
compiled_stream: {
diff --git a/x-pack/plugins/ingest_manager/server/services/package_policy.ts b/x-pack/plugins/ingest_manager/server/services/package_policy.ts
index 3a02544250ff0a..d91f6e8580fc3f 100644
--- a/x-pack/plugins/ingest_manager/server/services/package_policy.ts
+++ b/x-pack/plugins/ingest_manager/server/services/package_policy.ts
@@ -375,19 +375,19 @@ async function _assignPackageStreamToStream(
return { ...stream, compiled_stream: undefined };
}
const datasetPath = getDataset(stream.data_stream.dataset);
- const packageDatasets = pkgInfo.datasets;
- if (!packageDatasets) {
- throw new Error('Stream template not found, no datasets');
+ const packageDataStreams = pkgInfo.data_streams;
+ if (!packageDataStreams) {
+ throw new Error('Stream template not found, no data streams');
}
- const packageDataset = packageDatasets.find(
- (pkgDataset) => pkgDataset.name === stream.data_stream.dataset
+ const packageDataStream = packageDataStreams.find(
+ (pkgDataStream) => pkgDataStream.dataset === stream.data_stream.dataset
);
- if (!packageDataset) {
+ if (!packageDataStream) {
throw new Error(`Stream template not found, unable to find dataset ${datasetPath}`);
}
- const streamFromPkg = (packageDataset.streams || []).find(
+ const streamFromPkg = (packageDataStream.streams || []).find(
(pkgStream) => pkgStream.input === input.type
);
if (!streamFromPkg) {
diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx
index b43d6355c479aa..fc5ba1af196adb 100644
--- a/x-pack/plugins/ingest_manager/server/types/index.tsx
+++ b/x-pack/plugins/ingest_manager/server/types/index.tsx
@@ -45,7 +45,7 @@ export {
InstallationStatus,
PackageInfo,
RegistryVarsEntry,
- Dataset,
+ RegistryDataStream,
RegistryElasticsearch,
AssetReference,
EsAssetReference,
diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz b/x-pack/test/ingest_manager_api_integration/apis/fixtures/direct_upload_packages/apache_0.1.4.tar.gz
index cc983f6ac6d1aa53ac1b4a5b38b020a072a2326a..9cc4009d35c31da9e555cc98a0235f55efcf565e 100644
GIT binary patch
delta 461888
zcmV(oK=Hr(yd&eUBYz)@2mq#7a$f)g?7RhBT+6aA41pLV5FkK;hakayaCZ;xOkfz?
z-3bY0=`pfTKezw2k>a<80qNg8Gq<$XsGe1X=niq04O|$
z|0aEYf|Z4(hWV{3`e5V#knfgFTl+sq|51;>=>H+m_`ChJG%PiM7MA8<4WoZ$U-+N2
zKP?@Akp@7|aBF`Cn&0;S_uwb{QvxaZWPmc3Cg$LOrHtG50MOIlZGRfZfAoKP1_pX4
zJnDa?5_jSMa)0|n-A8)}bq@+kTmytBDTAko_XFRaK|Q{`X>V_V+xy#h+iw~Ei$g&=s#u$
z*jr8$C=isz&l*xd|2^#vo&J{oOD+CN|4A;?A97)!^q`EObbgZh7-RZ>q}(C%uhd^k
z{E_;Tynn}d-~K*;GPq6Cyln+zD2<=%YN-B?>37Kf2cBPQ@DtC^?Tz;@>Sn+3zagN%
zlSlCXYt{IT-}sIH4e@P!!23bDe$e;dDfEx@dp{`b-|{^BLGgZ2t{?RMcMAQ3@7@mz
z`-49EL7)Agct0rD5BmN){V`}k|JZy$|Bwv*V}FMN{o{ZF8t(_?`Y%=aH-6*)8o+!f
z&%Vw7{CDsFf8#fPZ;m{m(#8Q^Q!}UmgbZUyuKwr=el|9slv~z|ZkNCLoLd
zhJPTi|GNF@0JOBf?f>tUsZ-s(wO~K}RMqp!0
z4a3`~Mkdx8n%{J>G`9l7flQ3G^uCqxN6znS7s|iXBUWdmWTvDhhPzb=e5<0aiGSq}
z2^Quc7Q8>ky6<@?zSon|)K~}ZTVUHy_5DfvZ>bh~_P2>Nv@{I1G&Ic2-*bGgm63)H
znCg2ef0y#Tc%ZJOrKuLs0&IS(BJj31Sz1}#AyPrZ
z{;ng~
z+}y;&CFc?qfoN#Mk`XmF@^=vV2$1hw3z8WZxEm6gHnCy(JNmS`&wsdTa0b+znPp
zR~Ro$Yw(!WM%$*SYJB?e*-=d|uk~`cMekMjrzyLWDOTeb?BiZ8y0nz>dx%G|#vxFT
zKmF>Gg>@xG?SFiM2Y-$H>¨`~CcA*ykP&;rFmYzq&}>pqlgG>boRmW>quegSYO@2P>u}y##!z0`ew+lBzzWvb-vsajOBEx6|z!Vg9zeL
zQX+IlP*DwkWfK{W_W!lwpJN|6p3Zqj(&->X1B`s^4we(Gw|^#<24qA;;7X0Bed?~y
z;rsjhSBElCa%<1^w-P)briPfis>3FW9AYsVWA((d`&z8@%(>iL0FjhYv-crhmmN%=
zktnVEH*qWZflro>4UMp{?vWC6w598lCHr@q6HVShD`i0Ic((tq!8Fw@1g)G$k^eO_I?y6u-p_H)EdxC*UR=%JO=QJiV6`P
zowCX8F@MVLkc{LjV&c{{#ix(W&Q59umB9+{Du@wVo?pkRSx>9eh^|Lx8RQMd`SS;7c)12zkQo7-b>G*bd`>M67E=d(_H-=wW$kA0@YQ}0W
z&nwF34nBxVVohCHWTv2kd{D*W$N3OK+t2y66@RSoc#^YW5Eo{LJk~?MM_=0=G|*Os
z1kJcu>V)J_SBFYNM*Qw@k_JGb%u(~)x6-6#h{TuLwhh;Y5Six&M12`dMwU&BE8O^5-&CcJwn@3_j^kW`jT&kh9p=aPlv_3m`5D6FD-c*
zW*|sN7jI7lz4m~CnY`g9FxaYIo`CD#_e4%_hBnnsZI~@ARo2^6YHX1q6AeGeg@Ew7`
zAzQ5Wa%sqf3bRZ$FO2*&1FQ+5{Y0_7{vKcoMY@qllC>aOG+~+zP6iV1>Kf#_f0L}+
zt8o7*8fUBWX{Jh9F51;_zDDCbCTR+P(p1?(YoI6cSB|SgnZxyA`1ba8QyZJsV1Gi<
z-Mv;^gL`8aJ9UAia)vt(!RLz~a9)I$;Yfu{pbf}K`KayrH^=sm&NT{AEtcqgBn=9p
zUJhv3ugObt?*yvyua)hONFlit>GY1|N=Zux6TK%f@x_FeC)mrvr%Ayq+})0bbe4)5
zg<5^uTR!U3`n<_xcs3__sB%tB>wkDfP1(g+SLY|%_FQT-T_ER@%_=m`ErlM&(eZM2
zVZ(rg&c4#1H7Y{us=zaJxQ$BYJ_pXa>V$f5=`iNk)?*w0P_R%`laSr%*E#|5;U)m{
zq2OcVOo*yhnuy9$NEmL^yk}ir1VDbYFk+qbDP|q3-+CjE7i6_g-wsnrQ-AzEUwUCL
zbq1!S5Z
z$`a(NhOSIxn}_UIB#_KuYj7O*Vt;^w>wrXin&3$S8*Ye)SmPnD=N?bj$tug`%7@w*3m<9;
z)O9lS2hQvBO2KxHrJy!kmHG|cXRP`x?<^=5r<7__$p&53duI?WD2h1mUzF6^%;xYB
z=6yK%H16kn@ir@I+Q6EFsckTdJCZqkthk`9q^$ND4Sf?qb-Xa24u2xJp8+I93A{Jj
z-NPjL*MPMDxD9Jq*A}hnb?ft3vM*HWT`nf#6y!<>E2U@JIe|}TP5LHLV>wykEiy*V
zAyymAp$C;`7GydH&lHSuoG8}J=m*x%hYdc{k6um{atpLkqiVWb`&!;kCD_Yk
zZ-j0!*07w4P)Q~n9>EsPPNX>s6VOBn?(@kxQmyPR*EPg6h;~3*I?E*J880y-aJ>KE
z!Nf3W%pfY6{On?TNObu~iGf1*7vxHhrn~OQqUd
zs^qU9oog-3U07>d#ZTOh*k%YJ_PU(g7TGVUxezx$tyE>G3C>o1MAf2_4_dMb
zx5^dQCfOF;;%Ik~Fh8p5sk|7#Dl?w09J;5Nic&WNykZRw9u#Rz-7Ssx??2g&DcD}%
ze{p!AKJ^j+#ebnwI_DuNv4AdWfV=5r{G7%2k+XTH2yAYV@^i%+4%)P*&+0p{T?BEO
zy>w1AQ!}Q&e%4kzdMBx7i(E;S2lU;uIEcWZEU(HtO!p;g7i3IasfI^R1dI5uFAOZe
zd=@s#y;M4WeNXaYuG=?a*DwuJ*=p4b1ajQ#-f)?#rGIBEV%R!Bh@pE({J5$BzXA@Y
zA~*G8!v`gP@2(DHq_j~ym{R_!EMI?lZRywYpeCbUo?4WFfv0(tO%BNudadflbGA0?
zabu;&`PRl=*dNASYTUQeH~~Q$@%5`DXGpGjJ(C_%*u!Pd&)~{O-a4}p*A4AF@@aL8
zKk@yt|6
zn@Rzb6uQNMK8G*6iywD*GR+kBlk`z3XJ{A8(YIXzT{lZNK8~!`GF;gUmv~NH+P%Go
zaqiqG95e~|7cHX&3)6Gv?1u(>Mhih+%4I#$&YhlQdjs#6C(fd#AsT(U$~UBwqxzEgR$dorThT?|yXXh+2B&}Etetl+{NjMM``>kxKilr+>E=P=c72VC?SQU*XS0jKXE(gq4wB
zoecU?(dZb(XcKBzq#Vq+Syj3EyhNZVRlltE2t@8!0OU@TL=ER2sI{OU-^((Hu7B>V
z*s#kBVXl5eM?+>@Kg;Q%Vc1orh_#m3n+!%+H;h+(z?)AKgj#{*D}`H><3=S{pO-tV
zHI>P~SQ)XzyhJS8^K@{zgRWlAT>TVlWUYEaGiMW_aIv6iQeuoj#fBjwK#3L&6as81
zt;#}MJL@nkN=YC+7uniN4Q8Its(*H)8upx65FKZP2a@)(_~WoEqtwZ=3(iMasZD}b
zPHg47*6$shF1c`c+c;F%J0A&@)hF>rhS-2CvqF69x8V**w22#!puMytcyDKKuYci+ntv11AL9RJ
z5#lo~=!d$uGrlwWeBHMsuLVD?OQ_(Y>jB6^?W*fF8KFzbUT{aR58%3lohR>gD=u46
z-{(ilF@ryl|Zm(Qzid(0|`)9&Oggq&mTIexP2{>+9)fBEnf;bfRAxdjX=4z8IcR
zmoFvUaTp$jTj=-A+~Hd=v8Ww&jB?_`jFWHe4+L>enP45%g|*NNx{ug-yl&~=62Lss
zS=JHg#ns+Cx;9Sk=;-Piy3Ua1ZDMS(u7@vi$rO$u-2;2u+#*2tSg;MW$T*VzylpkiOP(Nk8G`0te_^a_oeE$Hs;q@&CShNo=y
zyaQF`XS87Nkx~to8-HA1X_{p3eeveyIYj`K?*pZ$Mox-ri)jaKaW0L^q^#nwa1H##
z!v&5P=!ssAO6X}ksd92cYRRwyod@Zup1dUkt$ws;4b$^dkA&1&$!(Q?GK2frcSF(V
z*wt12&d(n;FV4Td*-C7TrFo2U@%6%enS=sji5J89aZ8g`QGYL8hRW&KBsu|2#x%zv
z7YmcS(foF+;fti@A$q$V;AURt*zZ%w7)qF=|fW~&{HJ->#4LH4zN&pXvI+a7_-=JV<)X{y1jtduXWf>=vpM}jMtSpVlnR%
zb9yxTjVYc)f=lH71Y4OzjLM6UK+N=F1A}UplIWrilz)!>7;wP(hcP*wb4TfJcsU<+
zM_=uYs<`vm4z`t^ga!TKj@4GPg@gHsXp@`#z0ZK1q8EVfMDhcND!aV-IYR{88ry1s
z2XVv}atv(*av@aXI#N+xeoZK
z#>(gjmVfnfS-bNqn*%a3KNrI(-Klaje>49|?GEi%7J3_dXxEN%q{JmHz3eBk)>p$f
z-Z93N?Y4XK%>$TL8rxB9rPwn3KX?5WDageq!vML5(kL
zbbp*0OrEd_A$$_Ik+I}gyUIlRyfMhXyI;7l&LNx6*G{wHOV2r@P|3vHoD_!_i?uq$
z50b$d4L+L-w2@N*f>|G55PAt6p(=|y;mTn;wZbmgTgas+py!+OvrIrN{10ksV@t#C
z%v
zgEy=*t1QP#mZq%lbNbKA46O*xN!wV}4@me!ZNg-GmHMkp`^J!Dc4|d@@ns;$3;+s3
z*`?mdR`2Llz`T5UGoe0`YHn*r_6CVS(ro<9#7juE18;wAy#+&0_QJk@G_iWDeb#bv
zS-#2CSnt{kRkc7NXwKXm
zT;y^GY9O3cS3~({+Fe9cWD=m`0XNKpol)YM&;l3FMw-2rg*a
zWDE!`)doleNMO>EHPf0Wlb@%l{8_DCPX^MdO=Jk9wV1iy3N0i}BUX}J|2Q{ty`hFS
zDYvT$bd^6CJI0Mn+7}f~6#!X&()pl=OV00B{iY+&?3uFeg)7tTj$r!FW<
zE3O2WwoAjAS5!}`N)e=omwy)vM-6VIUXGPse(HkOlKgBZ_=zqBeM)-x@&teV829N(
zPK+tf!SrYTIEeNHX|ljOL8G3y&*5xgcKWVsRikrp!ri;)O$FtpDi5m*i8=;+v+VMs
zRaj#hvsN%cBU;r=g;}LmZa(o;>ulo#cK5OQOfGOLa%R51q1*YCXn(s?`rNT%D`nd?
zi`np1uOOHMR_Wx0oh}ku+qLlR#~giQNp(ViVMor6>S
zgD|0NbPq{>|JD`FFMn%iZy;sS<$EParx(L#g#seMe8tWMNf_}pH<>&M?y>BUXroCs
z6y^&xom|zHguT5z<%8=gZqfiQ%A(xHk>DNLU0ba)92IFpOWW6Rv
zDw598!Jg^j70i?82m6q^cV(N0N7AD#Bjfy>J;;gMwG~YCiE-H^BR7%u7v5ga7rNYP
zS2r5hBpc`TgntC5-bw=T(-%{{Y%3miiOy9I1q)aNKlZu6sAeT}SyPCat&h$RC4H6r
zT3}U_Wby>VFZ%I@M)qjuRNvA{VmCy8sn>7QU8nf9Qm6AfKtsZs<~aQ|PbQ_MUNfy2
zr;>2GKVQR0cjBtigfG*a4R_}0>SRQg6G5Q|%d}ul!GEPRLpp{(?;Mf16-!U6UT&US
z9+C`Tewc8QT$HC9+{2+3AhX6>n4qG|RVx{doenZ}#;&;sW?tEhwl(Siy|`3HEylTK
z;jlMjie~B@6_1*dpSvimHgP9MF&u>E<%3lr#`IH`A8xHml>96VElsf)k+akrcpAN8
z(m6TlQ-6>FGw5k?BX|Eu<%`36WZr|2kI_6il)h~n@;OH~z-tVbxt(Z|V@nE~qWnTJ>A*1VJVaxJM1|frLYk7SHwK8&
zg1=AOU+bdZU*`1G;u@UXAUbPH3DpYmTze1@;(yjCyjaeT^QTgGfGd0IjA~c^nr5XF
z{m?Gv3gT!%MccbJT@rLAb|FhwbhK0bF~mxIlF8PpzVF(qxb9#EU(+_%0aHeEe`RmW
z0g}?qYoSFNJjmLo&+!%!dt*_*_XPkublBSl#c0Eudx0-hWskOn-GYq8t)mn@4fR9M
za(@lw%C6D_Ey$EW%+6w!(kUBro$*@W)VvH>Kqo$#wTxKhs?o*k`hIqWd>lY+uJ*OM
zgbH--a(-IY;^tYKVO};z(c()RA9CXA*2`)zZT7Jq<+*BQcD0#tYhxpfNzGBXyhfwP
zQzq-pNTT;nS4X2?0piDO6n{JoB>Ob#O#5Mr9#Pbt2jNK}E;qYW^KhlvM#;iD
zr+Z^clQ`&Mml7>5#U_~+q25gt&$<$GNEj8%%f+L#b<8kAQIGKw`gd22C}!4?d{fnl
zH?$(E=z+C)F*R>p3S(1IPUmwTOVLprt3GIZp83x65Ss`cv+
zE-o8c)zP`WgzM~qc$WyKCwg~?sJdHS)eg2me~fXADOb++Ct;G0I)qi!V?vuWpLLA2
z&!LL}BF`B-oe6d-_87bBFDwN)sJ1aVh#HNvl3$9Uu_#Yk0v;%gN{QVU;u~3s@oXy#
zty`E23Y`52t>zAXoqmP}?0>yXd|RW`A>$kTIoLo`bDL5>D0KmlQu-k{{aUda6v=
z9$u!oMqRNF+~y_JA1>Kxk&0)hJM%dH%tyzO<2>H!VhM^vvqk}U&a6h|8n)wx1z|sl
ze8W&wxIVwuT$iZgWtT+O@AYw~Rr?J6^+l?GBsBtzJ$93o#5NT=PJh1FkJGek2iX1f
z#Q148`GmG+g9Dxr%0$jR)R2IQtRrtK=T;OF;{11)&n_VwAAq(f@V04?jsjd!K4bN
zAE?7zJ5HWqsk7^h1%D>y+nFl`P<@_sqK$BQX^^z@{93-37=cBqmdJ$+$!;H+T1_5m
z9$}Mz>zNbJ^6G?gS9x@VGHKYBEBA8ueQC4hfoKB7uRKvYOekNMd*U_@`DF;Lc#I75
zX4X%48xeVpiu2?dGD1T_J~sQJ8`!Ns{nR|U(|N?68_$N_y?=1=yhXt+Z?&Y)OlgdF{3D#~hy#idpbo5_~Q{IqJj#`*pf
ziMWSPH728R?6LGca5_Olgg)_{xB`G5Snf5BSr(CpB68Z%&S!aPM-a#CdZpsr{FDl-
zX%e3vyQa#(8Gn+Y53Ti}XXo_(Rd6PrB3V0EQ$U=Tx5Azx1JalnKM5H1Sw(T?F=q;-
z+W}GI;4vyvid)g^!o*tQT)i#Comn(no^URXt7!xRaHk4p?`Df=))QT93_Q))wMlHx
z>oJ?8o~wT029PZ9qNlCV;h0$l7_c6_Ux>Ki`4Q)Zvwx@qdRRwk8(BVMZZq^0t5FTP
zcL?Xi{&KjNTGcayf%Kt-u(e)_ua8SK62zV_b6&xjPtF8BJ0n9L+Hf(wxnO21JN|T>
z<`){OxH*9ubdyytKJ$!ZOJ?4BJ}%uxWHI7eu!HO~%6d6J-nkz+8NExE4u<^tcEYaU
z%v_<5hd!uO@$)#O1FJtO#5s>VhOpQJXW1G?8
ziDHV9hqG}ikvr%?$}02jN}XT1e0Wb~QYmkfnF)EdoNClsy()i>v14RtZd;*5y&(M+Kj
zEq@F52@#DZF@ivrg9;jJ*`vNnx-Xo%Vt@TSL-_h#=o@D{nA6c1M1un{)C!uVtrt-$
zndRF_X|_@6ID8+X2hWGOxrER%n>rlZ+GEKenZpXBX=n8tC$#}Eg_hDa;uI3s+k(0W
z!gjjOY3W^3JB=7oHW|#uBC#9~JImx~1b<2TSqrQUQo6Rll$19JITujCs*2a2131tX
z)XQU|n4%h4_iOuBpauO!B3DqEp5>J>&iW&Lq&6||0|1WFTD?B@PpA$~7{0xEC`EXN
zniF3s*t2;KjhOc(z@h4)vRqMRseA>0RD`k|)lnkb#OkDPQ}Zi{s}Sb8N^xI?2!HE6
zM>+k6TTdeIQy(A;jqeCop;-@$I8A)!hB5l+R5E3DdAj!k&qPKxL1b4NNq1d_-=m^x
zgX&??&RhN)!VZfl&CK}Tp*p4m-sljAsd2WM;e)KrIJeOUPg|3!HBPNHSqZ-Ahz%!P
z2AGCc1~}uWi>A+=mB{UR86qZ2Cx4#AhODR*IZfX5N;r>3kwx_Dn48lgqo6Pt5Te$~
zf-Y>o(3)2Kv_EoiDQa#Q#E#M2qo|q9%PBW-_jJG(XbZ2gmAtNR
ze27bLL05Cu{~o3erxjc89L^s6uIEeqgn6@&Z^I~I5;39mBakn7#J6Ah$dy|>{~#V=qQJtXE8K{4a;pC!q}7BwLULrSH2Msb
z>3Lw)A8~VyqT@HcYq^nG>H){xc|(_0y#Ar#Lq&yD?HDa_XTg_V+}s#{^lc2!`qA~u
ztcg~GGj%m31qXs@q*9pV+gG9rAN4DjA5_GtyWR^=6GK=`kzD0(pCCguiNO!^f
z6~jPw58+0>lWi_8a660&Ylp84J*BXVq-7d{vJxm?#;3?+r~^j#knIX%-jHV;WFs)4
zu2qFdPB<_}S*QhG+PRK@ipDh*Xz;a}-LCiTih66(j}TP!l{*0>F>mq0CZfwhd`;gj
zImNJI$uvN{uYa~eZjECQ-=#q|tE?ca;&cHwx9-%M`r91r8s$Sz<(0)d2nmhZq3@zdEX?RV3&7zX!ZpS4C0}jy3
ziZWhCCONiyA?~2t=M2VoX)8~k_4CEUyaLO#8H|e++;a;rLK*C~gW(~7l5OJxL#GPv
z+edt$9PkU)b>U#KKHe+Ufvemtwm0mk=%)B3Q47
zUNT|m)So{fXk&!e3o6{)O2nSnFyP%9dv3VEBzRJS=b!^JRfRfcE5a`(S4@3RQSornre(Q|QFG*zK=}D&you|@
zdN$t>!Q(Uf5~;5~wA#K%IDbV4mE(QxC5i2(P@^w?UJZ%dAn`X!hN{F4dXx^HP!oIl9Jv>A8y<{P9a10YTB}F$4;G?n-%fPYp2
zpcYnPu5uAB=1h{Y_H}C_hF(J!(>9Ea`Jlx1
ztjEzN++@Y%G4YeiF#KbU^?~v7O*V5UWm1ip%
z1{F&mC+tBFjW)CQL;s2_8iVy=On*1kni*zml#8mNh_E7SXZxBOcnnj>O65E`0K{ZP
zS4~iUfqu;Z+G`DlSm{kHtB`C-^^eOVxQMdO_~`9~hAqvIu6hmv?q{KO1%U@17ov)E
z4G89sjiGmH#o!9IE8*U*VaDIuHRtSfN!YCbk@m>cD4VMGtp(FuoSSZQl4HWG0k9u$cSaUyGA(
z*KRN2Co5})2Y$9PkV2gdz0P=-ZPSjf1G2(RvvbKh&C!C8R2;r2DhKisL3*yA^Zs?c
zjd~<|I*H@LZka9Sme&CrRDb)+Ftl%xXcG5ONuAY%BpTB(%M>J#4}E?eO(rx86#UH7
zocFS@S0R4$q?zvy$z1nPxjzfdE5u>QNmbboOP>{)PI9<)V&X*I6{jZjIqA{!HKRcG
zk1$4;?LF=MniF7xw6xc2=0ubo1zFFX=J1`zDo5qQ%qc`AB?WnS?teXbz*}+5yYOoT
zc1;MWK`nkFbj8cvF`yg%axuu-j5HSzRgi)D6goHQ-o}K{aX3!r?5#$NupVH(-lyPc(W1(dKE~|SN1EzhURCeIst{Gs
z6;92xwTT>^oXDzy)G+GZ6|RY+
zZSSLM$H~uy$8pDZc7?BmuC(ZN19U~7LLzhq^-%2^=Og3ec8}E>jrQ$$?(A|)!e5iK
z{ae#hUQzX~_J0{B^x?4qQV=lAz
zH_Do=U`HF1s5VKS(wMVf&L{bc(xHTz&`}W+(kNzkHh+wpdG{q%-RivHIpizAPA@79
zY0`9T8ZP_m!A8-?*Qu}{K782i<3+5809{Blb9MPdU^k|#80B+Sy-N&6!#CPG4abv}
zwj$3oGwzG~EMComii%ewFFMBrJ_t=w=>
zq^2-HaeplSi#B9&?Lv9MWP_+Jm_C6VbSY3|jbFVR0U0kP+g8%1wAN-qa3skk8KEL{
zm6_bVDqWSvAb~8;%*@!%x;~s;S@A?AlYcjmB5b=^(7M}BoPbA2ct2UdpGQ$qv4TZP
zdtQM@zjAOX;IZBswbshm6RrI|jz>l#ZA5vrv46IVINlJ(+^>=6NYSjKGhX$t4HWPl
z`QCv|13CV=OJ0sXwzW-4rTzl$<=)TpoCZ0!5E7`d1a=uZ6p#w{+7M_m8-XB+EH){{
z<10(}GLAwb;-z%6Y{0zL20?G0Vte62WO31P`C291n8`5<+9K+RNhj?z
zb$=dhkFjEt^mXRKy!~zfg;BPn^pbL6ojRBHI`hj;LW}!b2VYx4%IRo@1Gd(6h#y<*
zbyt?YcEMUwDkJ&0V31q!z-oP{-s36et;_1G-i{@t`y0D+ji;MMy~PW2jh!}ur?g7P
zk#8CzYRbXOptkm)rGSX;bU;D~_$KeB`+w@CUE!0$0fK~c-C@#~z7P2{4m(jk?e0f-
z@i-kCDwhqE3km*W7@CAN!PGls+8bMg39%Q5v#@`PxIchgNxJ|g=zj@x66!X}ENXEXLI)bZUkMt>xW
zQV(Aa=6MD#a=tz@i8;U=ZcS}{_!Zbx%%4ZqNY06C$QDvpxp~B`wxDUL&8?IQr~VRshf4=o)6g#Gi&UP2MmzKThJeg`
znAXj(lelJf8@ik>o5NHSW=dPFWq%4Ss$6o<>g4$r31$ot#N+zNakDz^nz9QBU_Galz<~93Maz3z4eMyrtYqM42fzcEID~9
zf%G@SK!T}wf4+@AYQE_OPb%OUJfhCB$z00CmOKAWb~82(G_}MQnUVR@wSOYPi6UGg;mfF>RT;145}BlyyBeBbBzNYq|ke@~Rc#*)Z*>7_pt
z*~NQi(WmZ5Z~UI5css70eaXHNq-u8YN!a6XIk|X(q*4sI&pzAnF4}TSrf_US%aJqjL
z7l}yK>8$I&m9$6;W+rAkWFLAWg-JiRk$&-DHf{ENzA^Ao)XBqKy8|>>UHD19(~d8J
zMZIeGT@uL2?T-}1ZYYqi1U)-eEefs}z5Dc^>k>K&mo^#w%x>wDFpULJMmHW5`3d~h$6{Z
zoQgcRm@I6M{ZNzai@iZtrmw;=n*Jj!O+*Lu+M=)nANp(h^F7=+&j13AP)gYu`-DDg
z{n`wlEb;Fl?SC1sLdX%}16tPAyafg_c_SYIoz1rc01Bfal1lg_XOvfE*7S4;@O)g#
z)L%R)w@*u(oES+ox{d8VTVZA`M1J%(U;c;}sKv*J{jtD(pAhpxVoC7BmeafUepy70
zY#SOwc({f$7iC13B(7#HYow7*anl^~5~uc_?i~zm4S%t5BCEknxsV=jODr|yiYx7RXZbqtdREug2X1HIb
z>xujok(6G$ZD#NAs>vF!fhr~E?Gh?ud~2%00{TLqS*u99`x!1S(6}LDedxTDe3nn+
zCHk$Hdw-G1mP$jFDTkLa4N}+^Gio&~pb3)Hr*uKTTE9FO;6mYH>zenh23hvcb<4%<$WwF66SHOH+T39iL)uyg@Rxe1eHCas^jKNkCMg@l9@IGh;g
z)SVw~7+}sh_}G@|4W^kNB{t0~L_>*%5C#SW*G|!NO9eGn9~bV`*k3G|cm^_)>k$2F
zUHIPny&!06-t0wH4-rT;-$!U5WUk512!EHVXlN0o4;3Z7qi*Pp;vMje@H~irsPn4{XD53fy~|DhqLuI7G@w~bGDy_MXIGBDsmby%rU)}=p)%UH
zV@sx{f3Iq%w()dtfuX>lE0Sg+?SJHGefVQcjPuoS+9|8`X5W5iBNA&=xvrX4=TnQG
zW_5M_ggmVAiaI8}?9HoR@1WkkaGh8sP+@Z_HO554@egNYChFX;=iHlRd(5LZ)KuIn
zi&eTamRF}yAfN_2Lf5f;Fusdm9N?gJHZ$8>HXB>*Bvr#rt=TFO+b!?JVt>5Qfr*Rj
z{mF;J+i7o~sCCm~jCSXdGY#Xep3mN}|BH(&lSg$a=RW=6^W6Kmdl8ovS^1LTTA5|I
z@vN1+;X2A%bOp1vVNnJ*-PoP!>yv`#iu=Sp8D%u;aINSNQDFFz>vps)189M}y_4U3
z2#w8}%k71X*V~xw!UCt|`+s6eR$LA-pfvK=Q*{eM&JRa;qDaaXzCw*Zxw+R8oACiX
z*B}HqYQFd(&qXlXAk>p}l+bH`bjmu9`k~Vr(G}7FK4MI7JioNZZdr;U(lZMUrVWXI7ggjUKsdnXR!|
z*Yabde!16RaSgDU@`4n)@YdO}k_&k}U}#8vC@!VU+&g5g;4~4u>9h1PO>W?n}9oHw^a>aL~9=C>b6|jwI`g
zY2s3i(&Jo>tFfQPO>s6P=)ouB?^wvX>|B7NHWVx-qrA)YhG$W+M2t~q!bQX_OyRI5oZl%F(J
zNNrHRQ(~H#M$1(>Hh~=v!O?8S4_>>Ume9>Ii3kXD6egSp87ah>2&YPVShXmh{q=vr7e**~%7+s$0D2
z6M~U=zqQk|!G8@wSjsDukF0(?pN0!Xvf?`r-)?QBHc?)rG*N|B+Ux|FEws!HN^+Lu
zNv~Tl`o(W5HW$(wsj@Yihet+l?{5XP_5%6h`wMssXyzAZKW$zxY+W!&h`?*BT*&3i
z_ImsH@R*wB7ilj4;*GnnoRM4}d_-XH7cy!9bw(3=V}G20?6!_@+Ay{pz?T6Ff_ago
zf}u%7r<_hWJ&>`%O@<-!VZCg>22eUYC!E>dcV~oS4AaJZyDLrHYWxl7Yw;FE8NSa?
zO=H5`lBs62QI%2;20M%4RS%(e)TH7*X~W+qa8D9;@wf!2|Z^-CKoTS
zv)D|RyMN~pBE%+M7P823_~`0Z1f+3@Gz@iQUL_7FXm_=ZdTw@#TI7av&Nv=)3rR}0
zR$85X(3LXkiDe!H%b}lZ)LVOOVx>1kWafmkhRdExZw=1?uaTj{L&+1P#eUgk-Jk82
zAhT~~`Mg*bfkNVLE5;0$Yvh?w94=_ru=5X@vrL&vmd=T~I$Umtb6_vR{c
zrLI*vrmy~J^2S|lJZ3YMro3izI*Fg`9)G{jr8JUS#6YaCicZLYNXb-x2X3+MTS1j`
z-MCiJ9WQZ0BfojX~4vD!n{;ImoT|ZnRJAnI9AWi&CNnuVSm=U
zPDsMuf9*@;g+EGab8u!ZOqtnM#K~|P^Z;v@+8?dI&Ff;bSbp-vW5A>hs2$M%?5@Kg
z???0|Ggv>i^@&@etbc049Duyt2K
z6~5G`+sh%_B&s65^DWqUls5zMz<*}OPPDr!Mqs-1`0Zuo{^)z0Un>puI1~li2eTjiEn+F4s8JN`Gz)M%HpR-2gK{%)fafhBYt=?nIQo3228<+9;)eabJUo
zQR&N#+@<3wBJ!J}w!EtyHtzseXp9EL(M#9yx~-|u&%M0y|ZctwAm-BsEbDEV|N>!~z_6+M-O>`U!>0|;vf(#80}>FG?+
z7%?U!iv@C7s$u{#&We*=4HV11+