From 8939ee6c24349cb87685824594b711e13ccb40d5 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 17 Aug 2021 16:10:18 +0300 Subject: [PATCH 01/11] [Lens] Supports long legend values (#107894) * [Lens] Supports multilines legend * Add a truncate legends switch * Add more unit tests * Add tooltip condition * Adress PR comments * Apply PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../heatmap_chart/heatmap_legend.ts | 21 +++ .../common/expressions/pie_chart/pie_chart.ts | 8 ++ .../common/expressions/pie_chart/types.ts | 2 + .../expressions/xy_chart/legend_config.ts | 21 +++ .../heatmap_visualization/chart_component.tsx | 6 + .../toolbar_component.tsx | 15 +++ .../visualization.test.ts | 4 + .../heatmap_visualization/visualization.tsx | 2 + .../render_function.test.tsx | 16 +++ .../pie_visualization/render_function.tsx | 5 + .../pie_visualization/suggestions.test.ts | 8 ++ .../public/pie_visualization/to_expression.ts | 2 + .../lens/public/pie_visualization/toolbar.tsx | 15 +++ .../legend_settings_popover.test.tsx | 41 +++++- .../legend_settings_popover.tsx | 120 +++++++++++++++++- .../__snapshots__/expression.test.tsx.snap | 35 +++++ .../__snapshots__/to_expression.test.ts.snap | 4 + .../public/xy_visualization/expression.tsx | 3 + .../public/xy_visualization/to_expression.ts | 2 + .../xy_visualization/xy_config_panel.tsx | 15 +++ 20 files changed, 342 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/common/expressions/heatmap_chart/heatmap_legend.ts b/x-pack/plugins/lens/common/expressions/heatmap_chart/heatmap_legend.ts index 0f553c6cae1f09..aae80be70e0505 100644 --- a/x-pack/plugins/lens/common/expressions/heatmap_chart/heatmap_legend.ts +++ b/x-pack/plugins/lens/common/expressions/heatmap_chart/heatmap_legend.ts @@ -19,6 +19,14 @@ export interface HeatmapLegendConfig { * Position of the legend relative to the chart */ position: Position; + /** + * Defines the number of lines per legend item + */ + maxLines?: number; + /** + * Defines if the legend items should be truncated + */ + shouldTruncate?: boolean; } export type HeatmapLegendConfigResult = HeatmapLegendConfig & { @@ -54,6 +62,19 @@ export const heatmapLegendConfig: ExpressionFunctionDefinition< defaultMessage: 'Specifies the legend position.', }), }, + maxLines: { + types: ['number'], + help: i18n.translate('xpack.lens.heatmapChart.legend.maxLines.help', { + defaultMessage: 'Specifies the number of lines per legend item.', + }), + }, + shouldTruncate: { + types: ['boolean'], + default: true, + help: i18n.translate('xpack.lens.heatmapChart.legend.shouldTruncate.help', { + defaultMessage: 'Specifies whether or not the legend items should be truncated.', + }), + }, }, fn(input, args) { return { diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts index b298f1d8b3a806..7d228f04c25e7d 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts @@ -74,6 +74,14 @@ export const pie: ExpressionFunctionDefinition< types: ['boolean'], help: '', }, + legendMaxLines: { + types: ['number'], + help: '', + }, + truncateLegend: { + types: ['boolean'], + help: '', + }, legendPosition: { types: ['string'], options: [Position.Top, Position.Right, Position.Bottom, Position.Left], diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts index 213651134d98ab..8712675740f1c2 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts @@ -17,6 +17,8 @@ export interface SharedPieLayerState { legendPosition?: 'left' | 'right' | 'top' | 'bottom'; nestedLegend?: boolean; percentDecimals?: number; + legendMaxLines?: number; + truncateLegend?: boolean; } export type PieLayerState = SharedPieLayerState & { diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/legend_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/legend_config.ts index e228039b53ef6a..fdf8d06b594242 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/legend_config.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/legend_config.ts @@ -37,6 +37,14 @@ export interface LegendConfig { * Number of columns when legend is set inside chart */ floatingColumns?: number; + /** + * Maximum number of lines per legend item + */ + maxLines?: number; + /** + * Flag whether the legend items are truncated or not + */ + shouldTruncate?: boolean; } export type LegendConfigResult = LegendConfig & { type: 'lens_xy_legendConfig' }; @@ -100,6 +108,19 @@ export const legendConfig: ExpressionFunctionDefinition< defaultMessage: 'Specifies the number of columns when legend is displayed inside chart.', }), }, + maxLines: { + types: ['number'], + help: i18n.translate('xpack.lens.xyChart.maxLines.help', { + defaultMessage: 'Specifies the number of lines per legend item.', + }), + }, + shouldTruncate: { + types: ['boolean'], + default: true, + help: i18n.translate('xpack.lens.xyChart.shouldTruncate.help', { + defaultMessage: 'Specifies whether the legend items will be truncated or not', + }), + }, }, fn: function fn(input: unknown, args: LegendConfig) { return { diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index 15e9963ff57406..e3da4bfe7fe72d 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -321,6 +321,12 @@ export const HeatmapComponent: FC = ({ showLegend={args.legend.isVisible} legendPosition={args.legend.position} debugState={window._echDebugStateFlag ?? false} + theme={{ + ...chartTheme, + legend: { + labelOptions: { maxLines: args.legend.shouldTruncate ? args.legend?.maxLines ?? 1 : 0 }, + }, + }} /> { + setState({ + ...state, + legend: { ...state.legend, maxLines: val }, + }); + }} + shouldTruncate={state?.legend.shouldTruncate ?? true} + onTruncateLegendChange={() => { + const current = state.legend.shouldTruncate ?? true; + setState({ + ...state, + legend: { ...state.legend, shouldTruncate: !current }, + }); + }} /> diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts index bceeeebb5e1403..5e7ee1b8b097bd 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts @@ -32,6 +32,8 @@ function exampleState(): HeatmapVisualizationState { isVisible: true, position: Position.Right, type: LEGEND_FUNCTION, + maxLines: 1, + shouldTruncate: true, }, gridConfig: { type: HEATMAP_GRID_FUNCTION, @@ -63,6 +65,8 @@ describe('heatmap', () => { isVisible: true, position: Position.Right, type: LEGEND_FUNCTION, + maxLines: 1, + shouldTruncate: true, }, gridConfig: { type: HEATMAP_GRID_FUNCTION, diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx index 5405cff6ed1db6..62e3138f397dad 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx @@ -70,6 +70,8 @@ function getInitialState(): Omit { numberDisplay: 'hidden', categoryDisplay: 'default', legendDisplay: 'default', + legendMaxLines: 1, + truncateLegend: true, nestedLegend: false, percentDecimals: 3, hideLabels: false, @@ -106,6 +108,20 @@ describe('PieVisualization component', () => { expect(component.find(Settings).prop('showLegend')).toEqual(false); }); + test('it sets the correct lines per legend item', () => { + const component = shallow(); + expect(component.find(Settings).prop('theme')).toEqual({ + background: { + color: undefined, + }, + legend: { + labelOptions: { + maxLines: 1, + }, + }, + }); + }); + test('it calls the color function with the right series layers', () => { const defaultArgs = getDefaultArgs(); const component = shallow( 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 d25726951ea8fd..41b96ff4324ae3 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -75,6 +75,8 @@ export function PieComponent( legendPosition, nestedLegend, percentDecimals, + legendMaxLines, + truncateLegend, hideLabels, palette, } = props.args; @@ -297,6 +299,9 @@ export function PieComponent( ...chartTheme.background, color: undefined, // removes background for embeddables }, + legend: { + labelOptions: { maxLines: truncateLegend ? legendMaxLines ?? 1 : 0 }, + }, }} baseTheme={chartBaseTheme} /> diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts index affc74d8b70cd9..5a57371eb6459f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -494,6 +494,8 @@ describe('suggestions', () => { categoryDisplay: 'inside', legendDisplay: 'show', percentDecimals: 0, + legendMaxLines: 1, + truncateLegend: true, nestedLegend: true, }, ], @@ -516,6 +518,8 @@ describe('suggestions', () => { categoryDisplay: 'inside', legendDisplay: 'show', percentDecimals: 0, + legendMaxLines: 1, + truncateLegend: true, nestedLegend: true, }, ], @@ -684,6 +688,8 @@ describe('suggestions', () => { categoryDisplay: 'inside', legendDisplay: 'show', percentDecimals: 0, + legendMaxLines: 1, + truncateLegend: true, nestedLegend: true, }, ], @@ -705,6 +711,8 @@ describe('suggestions', () => { categoryDisplay: 'default', // This is changed legendDisplay: 'show', percentDecimals: 0, + legendMaxLines: 1, + truncateLegend: true, nestedLegend: true, }, ], diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index 7ee26383cebbf2..fd754906ceb021 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -56,6 +56,8 @@ function expressionHelper( legendDisplay: [layer.legendDisplay], legendPosition: [layer.legendPosition || 'right'], percentDecimals: [layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS], + legendMaxLines: [layer.legendMaxLines ?? 1], + truncateLegend: [layer.truncateLegend ?? true], nestedLegend: [!!layer.nestedLegend], ...(state.palette ? { diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index 5da69e47f861cd..685a8392dcfd34 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -220,6 +220,21 @@ export function PieToolbar(props: VisualizationToolbarProps { + const current = layer.truncateLegend ?? true; + setState({ + ...state, + layers: [{ ...layer, truncateLegend: !current }], + }); + }} + maxLines={layer?.legendMaxLines} + onMaxLinesChange={(val) => { + setState({ + ...state, + layers: [{ ...layer, legendMaxLines: val }], + }); + }} /> ); diff --git a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx index e2fd630702b6be..95739c294b3206 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx @@ -7,7 +7,11 @@ import React from 'react'; import { shallowWithIntl as shallow } from '@kbn/test/jest'; -import { LegendSettingsPopover, LegendSettingsPopoverProps } from './legend_settings_popover'; +import { + LegendSettingsPopover, + LegendSettingsPopoverProps, + MaxLinesInput, +} from './legend_settings_popover'; describe('Legend Settings', () => { const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [ @@ -50,6 +54,41 @@ describe('Legend Settings', () => { expect(props.onDisplayChange).toHaveBeenCalled(); }); + it('should have default the max lines input to 1 when no value is given', () => { + const component = shallow(); + expect(component.find(MaxLinesInput).prop('value')).toEqual(1); + }); + + it('should have the `Truncate legend text` switch enabled by default', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="lens-legend-truncate-switch"]').prop('checked') + ).toEqual(true); + }); + + it('should set the truncate switch state when truncate prop value is false', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="lens-legend-truncate-switch"]').prop('checked') + ).toEqual(false); + }); + + it('should have disabled the max lines input when truncate is set to false', () => { + const component = shallow(); + expect(component.find(MaxLinesInput).prop('isDisabled')).toEqual(true); + }); + + it('should have called the onTruncateLegendChange function on truncate switch change', () => { + const nestedProps = { + ...props, + shouldTruncate: true, + onTruncateLegendChange: jest.fn(), + }; + const component = shallow(); + component.find('[data-test-subj="lens-legend-truncate-switch"]').simulate('change'); + expect(nestedProps.onTruncateLegendChange).toHaveBeenCalled(); + }); + it('should enable the Nested Legend Switch when renderNestedLegendSwitch prop is true', () => { const component = shallow(); expect(component.find('[data-test-subj="lens-legend-nested-switch"]')).toHaveLength(1); diff --git a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx index 0ec7c11f6fdc1d..ba5e93c3f89522 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx @@ -7,12 +7,19 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiButtonGroup, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { + EuiFormRow, + EuiButtonGroup, + EuiSwitch, + EuiSwitchEvent, + EuiFieldNumber, +} from '@elastic/eui'; import { Position, VerticalAlignment, HorizontalAlignment } from '@elastic/charts'; import { ToolbarPopover } from '../shared_components'; import { LegendLocationSettings } from './legend_location_settings'; import { ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/public'; import { TooltipWrapper } from './tooltip_wrapper'; +import { useDebouncedValue } from './debounced_value'; export interface LegendSettingsPopoverProps { /** @@ -64,9 +71,25 @@ export interface LegendSettingsPopoverProps { */ floatingColumns?: number; /** - * Callback on horizontal alignment option change + * Callback on alignment option change */ onFloatingColumnsChange?: (value: number) => void; + /** + * Sets the number of lines per legend item + */ + maxLines?: number; + /** + * Callback on max lines option change + */ + onMaxLinesChange?: (value: number) => void; + /** + * Defines if the legend items will be truncated or not + */ + shouldTruncate?: boolean; + /** + * Callback on nested switch status change + */ + onTruncateLegendChange?: (event: EuiSwitchEvent) => void; /** * If true, nested legend switch is rendered */ @@ -97,6 +120,38 @@ export interface LegendSettingsPopoverProps { groupPosition?: ToolbarButtonProps['groupPosition']; } +const DEFAULT_TRUNCATE_LINES = 1; +const MAX_TRUNCATE_LINES = 5; +const MIN_TRUNCATE_LINES = 1; + +export const MaxLinesInput = ({ + value, + setValue, + isDisabled, +}: { + value: number; + setValue: (value: number) => void; + isDisabled: boolean; +}) => { + const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange: setValue }); + return ( + { + const val = Number(e.target.value); + // we want to automatically change the values to the limits + // if the user enters a value that is outside the limits + handleInputChange(Math.min(MAX_TRUNCATE_LINES, Math.max(val, MIN_TRUNCATE_LINES))); + }} + /> + ); +}; + export const LegendSettingsPopover: React.FunctionComponent = ({ legendOptions, mode, @@ -117,6 +172,10 @@ export const LegendSettingsPopover: React.FunctionComponent {}, renderValueInLegendSwitch, groupPosition = 'right', + maxLines, + onMaxLinesChange = () => {}, + shouldTruncate, + onTruncateLegendChange = () => {}, }) => { return ( + + + + + + + + + + {renderNestedLegendSwitch && ( { + setState({ + ...state, + legend: { ...state.legend, maxLines: val }, + }); + }} + shouldTruncate={state?.legend.shouldTruncate ?? true} + onTruncateLegendChange={() => { + const current = state?.legend.shouldTruncate ?? true; + setState({ + ...state, + legend: { ...state.legend, shouldTruncate: !current }, + }); + }} onPositionChange={(id) => { setState({ ...state, From 304bbf054f1351e273101fee328816ef3a2c5406 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 17 Aug 2021 08:12:32 -0500 Subject: [PATCH 02/11] [build] Remove scss files (#108821) --- src/dev/build/tasks/copy_source_task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 65edd6b61720e2..d2f5f8333ed9f1 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -32,7 +32,7 @@ export const CopySource: Task = { '!src/plugins/telemetry/schema/**', // Skip telemetry schemas // this is the dev-only entry '!src/setup_node_env/index.js', - '!**/public/**/*.{js,ts,tsx,json}', + '!**/public/**/*.{js,ts,tsx,json,scss}', 'typings/**', 'config/kibana.yml', 'config/node.options', From e2ad384d125e71f97d15dd79ff2a9edce37b9dd0 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 17 Aug 2021 08:13:04 -0500 Subject: [PATCH 03/11] [build] Remove jest.config.js (#108822) --- src/dev/build/tasks/copy_source_task.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index d2f5f8333ed9f1..0f073e383d2c88 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -29,6 +29,7 @@ export const CopySource: Task = { '!src/cli/dev.js', '!src/functional_test_runner/**', '!src/dev/**', + '!**/jest.config.js', '!src/plugins/telemetry/schema/**', // Skip telemetry schemas // this is the dev-only entry '!src/setup_node_env/index.js', From 848085a8c356fcae7a74b7790fde97656514f8a3 Mon Sep 17 00:00:00 2001 From: DeFazio Date: Tue, 17 Aug 2021 09:18:36 -0400 Subject: [PATCH 04/11] Remove styling from euiPanel around Alerts tables (#108603) * Add styling to euiPanel * remove styling on Observability side in favor of separate PR * Fix merge conflicts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/t_grid/integrated/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 23041a8f749c40..a7a80b5e61d2f5 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -314,7 +314,13 @@ const TGridIntegratedComponent: React.FC = ({ return ( - + {loading && } {canQueryTimeline ? ( From 04a8a3c1990c968d0c950d478f4a1873ae4c3482 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 17 Aug 2021 14:20:17 +0100 Subject: [PATCH 05/11] [RAC] integrating rbac search strategy with alert flyout (#108748) * add alert consumers for useTimelineEventDetails * set entityType to events * rename to AlertConsumers * set entityType to alerts * send entity type to search strategy * fix import * fix import * fix import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cases/public/components/case_view/index.tsx | 5 ++++- .../public/components/timeline_context/index.tsx | 8 +++++++- .../public/cases/components/case_view/index.tsx | 9 ++++++++- .../public/common/components/events_viewer/index.tsx | 5 +++++ .../components/side_panel/event_details/index.tsx | 8 ++++++++ .../public/timelines/components/side_panel/index.tsx | 9 +++++++++ .../components/timeline/eql_tab_content/index.tsx | 4 ++++ .../components/timeline/notes_tab_content/index.tsx | 5 +++++ .../components/timeline/pinned_tab_content/index.tsx | 4 ++++ .../components/timeline/query_tab_content/index.tsx | 4 ++++ .../public/timelines/containers/details/index.tsx | 12 +++++++++++- .../timelines/common/utils/field_formatters.ts | 2 +- 12 files changed, 70 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index a44c2cb22010e2..b333d908fa77c6 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -105,6 +105,7 @@ export const CaseComponent = React.memo( const [initLoadingData, setInitLoadingData] = useState(true); const init = useRef(true); const timelineUi = useTimelineContext()?.ui; + const alertConsumers = useTimelineContext()?.alertConsumers; const { caseUserActions, @@ -486,7 +487,9 @@ export const CaseComponent = React.memo( - {timelineUi?.renderTimelineDetailsPanel ? timelineUi.renderTimelineDetailsPanel() : null} + {timelineUi?.renderTimelineDetailsPanel + ? timelineUi.renderTimelineDetailsPanel({ alertConsumers }) + : null} ); } diff --git a/x-pack/plugins/cases/public/components/timeline_context/index.tsx b/x-pack/plugins/cases/public/components/timeline_context/index.tsx index 727e4b64628d1c..76952e638e1984 100644 --- a/x-pack/plugins/cases/public/components/timeline_context/index.tsx +++ b/x-pack/plugins/cases/public/components/timeline_context/index.tsx @@ -7,6 +7,7 @@ import React, { useState } from 'react'; import { EuiMarkdownEditorUiPlugin, EuiMarkdownAstNodePosition } from '@elastic/eui'; +import { AlertConsumers } from '@kbn/rule-data-utils'; import { Plugin } from 'unified'; /** * @description - manage the plugins, hooks, and ui components needed to enable timeline functionality within the cases plugin @@ -28,6 +29,7 @@ interface TimelineProcessingPluginRendererProps { } export interface CasesTimelineIntegration { + alertConsumers?: AlertConsumers[]; editor_plugins: { parsingPlugin: Plugin; processingPluginRenderer: React.FC< @@ -43,7 +45,11 @@ export interface CasesTimelineIntegration { }; ui?: { renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element; - renderTimelineDetailsPanel?: () => JSX.Element; + renderTimelineDetailsPanel?: ({ + alertConsumers, + }: { + alertConsumers?: AlertConsumers[]; + }) => JSX.Element; }; } diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index fdb12170309c7a..ddc739b05f4c26 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { AlertConsumers } from '@kbn/rule-data-utils'; import { getCaseDetailsUrl, @@ -33,6 +34,7 @@ import { SpyRoute } from '../../../common/utils/route/spy_routes'; import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; import { CaseDetailsRefreshContext } from '../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context'; import { getEndpointDetailsPath } from '../../../management/common/routing'; +import { EntityType } from '../../../../../timelines/common'; interface Props { caseId: string; @@ -53,13 +55,17 @@ export interface CaseProps extends Props { updateCase: (newCase: Case) => void; } -const TimelineDetailsPanel = () => { +const ALERT_CONSUMER: AlertConsumers[] = [AlertConsumers.SIEM]; + +const TimelineDetailsPanel = ({ alertConsumers }: { alertConsumers?: AlertConsumers[] }) => { const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.detections); return ( @@ -228,6 +234,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = showAlertDetails, subCaseId, timelineIntegration: { + alertConsumers: ALERT_CONSUMER, editor_plugins: { parsingPlugin: timelineMarkdownPlugin.parser, processingPluginRenderer: timelineMarkdownPlugin.renderer, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 108a744afc08a5..2c4241ffbbb165 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -11,6 +11,7 @@ import deepEqual from 'fast-deep-equal'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; +import { AlertConsumers } from '@kbn/rule-data-utils'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; @@ -69,6 +70,8 @@ export interface OwnProps { type Props = OwnProps & PropsFromRedux; +const alertConsumers: AlertConsumers[] = [AlertConsumers.SIEM]; + /** * The stateful events viewer component is the highest level component that is utilized across the security_solution pages layer where * timeline is used BESIDES the flyout. The flyout makes use of the `EventsViewer` component which is a subcomponent here @@ -216,7 +219,9 @@ const StatefulEventsViewerComponent: React.FC = ({ = ({ + alertConsumers, browserFields, docValueFields, + entityType, expandedEvent, handleOnEventClosed, isFlyoutView, @@ -74,7 +80,9 @@ const EventDetailsPanelComponent: React.FC = ({ timelineId, }) => { const [loading, detailsData] = useTimelineEventsDetails({ + alertConsumers, docValueFields, + entityType, indexName: expandedEvent.indexName ?? '', eventId: expandedEvent.eventId ?? '', skip: !expandedEvent.eventId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx index 3e57ec2e039f50..e264c7ec9fa041 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx @@ -8,6 +8,8 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { EuiFlyout, EuiFlyoutProps } from '@elastic/eui'; +import { AlertConsumers } from '@kbn/rule-data-utils'; + import { timelineActions, timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; import { BrowserFields, DocValueFields } from '../../../common/containers/source'; @@ -16,10 +18,13 @@ import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { EventDetailsPanel } from './event_details'; import { HostDetailsPanel } from './host_details'; import { NetworkDetailsPanel } from './network_details'; +import { EntityType } from '../../../../../timelines/common'; interface DetailsPanelProps { + alertConsumers?: AlertConsumers[]; browserFields: BrowserFields; docValueFields: DocValueFields[]; + entityType?: EntityType; handleOnPanelClosed?: () => void; isFlyoutView?: boolean; tabType?: TimelineTabs; @@ -33,8 +38,10 @@ interface DetailsPanelProps { */ export const DetailsPanel = React.memo( ({ + alertConsumers, browserFields, docValueFields, + entityType, handleOnPanelClosed, isFlyoutView, tabType, @@ -70,8 +77,10 @@ export const DetailsPanel = React.memo( panelSize = 'm'; visiblePanel = ( = ({ activeTab, columns, @@ -346,6 +349,7 @@ export const EqlTabContentComponent: React.FC = ({ = ({ timelineId } () => expandedDetail[TimelineTabs.notes]?.panelView ? ( React.ReactNode; rowRenderers: RowRenderer[]; @@ -266,6 +269,7 @@ export const PinnedTabContentComponent: React.FC = ({ theme.eui.paddingSizes.s}; `; +const alertConsumers: AlertConsumers[] = [AlertConsumers.SIEM]; + const isTimerangeSame = (prevProps: Props, nextProps: Props) => prevProps.end === nextProps.end && prevProps.start === nextProps.start && @@ -414,6 +417,7 @@ export const QueryTabContentComponent: React.FC = ({ { const myRequest = { ...(prevRequest ?? {}), + alertConsumers, docValueFields, + entityType, indexName, eventId, factoryQueryType: TimelineEventsQueries.details, @@ -114,7 +124,7 @@ export const useTimelineEventsDetails = ({ } return prevRequest; }); - }, [docValueFields, eventId, indexName]); + }, [alertConsumers, docValueFields, entityType, eventId, indexName]); useEffect(() => { timelineDetailsSearch(timelineDetailsRequest); diff --git a/x-pack/plugins/timelines/common/utils/field_formatters.ts b/x-pack/plugins/timelines/common/utils/field_formatters.ts index b436f8e6161222..a48f03b90af6bf 100644 --- a/x-pack/plugins/timelines/common/utils/field_formatters.ts +++ b/x-pack/plugins/timelines/common/utils/field_formatters.ts @@ -43,7 +43,7 @@ export const getDataFromSourceHits = ( category?: string, path?: string ): TimelineEventsDetailsItem[] => - Object.keys(sources).reduce((accumulator, source) => { + Object.keys(sources ?? {}).reduce((accumulator, source) => { const item: EventSource = get(source, sources); if (Array.isArray(item) || isString(item) || isNumber(item)) { const field = path ? `${path}.${source}` : source; From 609bd907857f3acbb10d9583e499c7072dabe6e0 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 17 Aug 2021 16:21:14 +0300 Subject: [PATCH 06/11] mute incorrect types in es-query (#108898) * mute incorrect types in es-query * fix another conflict --- packages/kbn-es-query/src/kuery/functions/geo_bounding_box.ts | 1 + packages/kbn-es-query/src/kuery/functions/geo_polygon.ts | 1 + .../ml/public/ui_actions/open_in_anomaly_explorer_action.tsx | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.ts b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.ts index 6a44eed1d7ec96..1dae0b40ff08e7 100644 --- a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.ts @@ -53,6 +53,7 @@ export function toElasticsearchQuery( } return { + // @ts-expect-error @elastic/elasticsearch doesn't support ignore_unmapped in QueryDslGeoBoundingBoxQuery geo_bounding_box: { [fieldName]: queryParams, ignore_unmapped: true, diff --git a/packages/kbn-es-query/src/kuery/functions/geo_polygon.ts b/packages/kbn-es-query/src/kuery/functions/geo_polygon.ts index 713124e1c4e936..cf0bcdafa04c71 100644 --- a/packages/kbn-es-query/src/kuery/functions/geo_polygon.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_polygon.ts @@ -49,6 +49,7 @@ export function toElasticsearchQuery( } return { + // @ts-expect-error @elastic/elasticsearch doesn't support ignore_unmapped in QueryDslGeoPolygonQuery geo_polygon: { [fieldName]: queryParams, ignore_unmapped: true, diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index 3111c7f134da0c..f4941649ac7a7f 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -83,7 +83,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta should: [ { match_phrase: { - [fieldName]: fieldValue, + [fieldName]: String(fieldValue), }, }, ], @@ -104,6 +104,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta pageState: { jobIds, timeRange, + // @ts-ignore QueryDslQueryContainer is not compatible with SerializableRecord ...(mlExplorerFilter ? ({ mlExplorerFilter } as SerializableRecord) : {}), query: {}, }, From 94f35087b0909c49b9b4c709673b105df22bf5cc Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 17 Aug 2021 16:29:02 +0300 Subject: [PATCH 07/11] fix another conflict in test file (#108905) --- .../kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts index cfa496fb5a2a07..45e6474b22986d 100644 --- a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts @@ -108,6 +108,7 @@ describe('kuery functions', () => { const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + // @ts-expect-error @elastic/elasticsearch doesn't support ignore_unmapped in QueryDslGeoBoundingBoxQuery expect(result.geo_bounding_box!.ignore_unmapped).toBe(true); }); From 8bfd5e2f956dc3bde006c7e76d7a174c02bdd00d Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 17 Aug 2021 09:48:04 -0400 Subject: [PATCH 08/11] [Uptime][Synthetics Integration] Add browser monitors configuration options (#102928) * update types * add browser context * update validation * add browser fields to custom fields * add browser simple fields and source field * add browser context to create and edit wrappers * update content * add formatters for formatting javascript values to integration policy yaml * fix policy name bug * adjust tests * adjust types * add normalizers for converting yaml to javascript * update default values * add params field to browser monitors * adjust types, formatters and normalizers to account for browser advanced fields * add browser advanced fields context and components * adjust http and tcp providers * adjust use_update_policy and wrappers * update types * update content * adjust timeout content * adjust zip url content * adjust content * remove unused content * hide monitor options that do not have corresponding data streams from the integration package * Update x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx Co-authored-by: Justin Kambic * Update x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx Co-authored-by: Justin Kambic * Update x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx Co-authored-by: Justin Kambic * Update x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx Co-authored-by: Justin Kambic * adjust content * adjust validation * adjust tests * adjust normalizers and formatters and add tests * resolves validation error with inline scripts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Justin Kambic --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../browser/advanced_fields.test.tsx | 49 ++ .../fleet_package/browser/advanced_fields.tsx | 117 ++++ .../fleet_package/browser/formatters.ts | 24 + .../fleet_package/browser/normalizers.ts | 43 ++ .../fleet_package/browser/simple_fields.tsx | 183 ++++++ .../fleet_package/browser/source_field.tsx | 248 ++++++++ .../fleet_package/common/default_values.ts | 19 + .../fleet_package/common/formatters.test.ts | 36 ++ .../fleet_package/common/formatters.ts | 32 + .../fleet_package/common/normalizers.test.ts | 28 + .../fleet_package/common/normalizers.ts | 79 +++ .../contexts/advanced_fields_http_context.tsx | 2 +- .../contexts/advanced_fields_tcp_context.tsx | 2 +- .../contexts/browser_context.tsx | 59 ++ .../contexts/browser_context_advanced.tsx | 52 ++ .../contexts/browser_provider.tsx | 52 ++ .../fleet_package/contexts/http_context.tsx | 13 +- .../fleet_package/contexts/http_provider.tsx | 60 +- .../fleet_package/contexts/icmp_context.tsx | 14 +- .../fleet_package/contexts/index.ts | 24 +- .../fleet_package/contexts/tcp_context.tsx | 14 +- .../fleet_package/contexts/tcp_provider.tsx | 59 +- .../contexts/tls_fields_context.tsx | 2 +- .../fleet_package/custom_fields.test.tsx | 73 ++- .../fleet_package/custom_fields.tsx | 39 +- .../fleet_package/helpers/context_helpers.ts | 17 + .../fleet_package/helpers/formatters.ts | 38 ++ .../fleet_package/helpers/normalizers.ts | 42 ++ .../fleet_package/http/advanced_fields.tsx | 44 +- .../fleet_package/http/formatters.ts | 44 ++ .../fleet_package/http/normalizers.ts | 85 +++ .../fleet_package/http/simple_fields.tsx | 29 +- .../fleet_package/icmp/formatters.ts | 17 + .../fleet_package/icmp/normalizers.ts | 31 + .../fleet_package/icmp/simple_fields.tsx | 29 +- .../synthetics_policy_create_extension.tsx | 125 ++-- ...s_policy_create_extension_wrapper.test.tsx | 146 ++++- ...hetics_policy_create_extension_wrapper.tsx | 5 +- .../synthetics_policy_edit_extension.tsx | 81 +-- ...ics_policy_edit_extension_wrapper.test.tsx | 237 +++++++- ...nthetics_policy_edit_extension_wrapper.tsx | 201 +++---- .../fleet_package/tcp/formatters.ts | 22 + .../fleet_package/tcp/normalizers.ts | 32 + .../fleet_package/tcp/simple_fields.tsx | 27 +- .../fleet_package/tls/formatters.ts | 34 ++ .../fleet_package/tls/normalizers.ts | 49 ++ .../public/components/fleet_package/types.tsx | 50 +- .../fleet_package/use_update_policy.test.tsx | 550 ++++++++++++------ .../fleet_package/use_update_policy.ts | 84 +-- .../components/fleet_package/validation.tsx | 81 ++- 52 files changed, 2650 insertions(+), 775 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/formatters.test.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_provider.tsx create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/helpers/context_helpers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/helpers/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/helpers/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/http/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/icmp/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/icmp/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/tcp/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/tcp/normalizers.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/tls/formatters.ts create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/tls/normalizers.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9c6dc3b591a9f4..621af3e92de8d3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25168,7 +25168,6 @@ "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tags.label": "タグ", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts": "ホスト:ポート", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts.error": "ホストとポートは必須です", - "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.error": "タイムアウトは0以上で、スケジュール間隔未満でなければなりません", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.helpText": "接続のテストとデータの交換に許可された合計時間。", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.label": "タイムアウト (秒) ", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.URL": "URL", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1465e6d0a1952c..c2831d0aa75185 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25723,7 +25723,6 @@ "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tags.label": "标签", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts": "主机:端口", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.tcp.hosts.error": "主机和端口必填", - "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.error": "超时必须等于或大于 0 且小于计划时间间隔", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.helpText": "允许用于测试连接并交换数据的总时间。", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.timeout.label": "超时(秒)", "xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.URL": "URL", diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx new file mode 100644 index 00000000000000..aa1f7ca07e3d8e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { BrowserAdvancedFields } from './advanced_fields'; +import { ConfigKeys, IBrowserAdvancedFields } from '../types'; +import { + BrowserAdvancedFieldsContextProvider, + defaultBrowserAdvancedFields as defaultConfig, +} from '../contexts'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('', () => { + const WrappedComponent = ({ defaultValues }: { defaultValues?: IBrowserAdvancedFields }) => { + return ( + + + + ); + }; + + it('renders BrowserAdvancedFields', () => { + const { getByLabelText } = render(); + + const syntheticsArgs = getByLabelText('Synthetics args'); + const screenshots = getByLabelText('Screenshot options') as HTMLInputElement; + expect(screenshots.value).toEqual(defaultConfig[ConfigKeys.SCREENSHOTS]); + expect(syntheticsArgs).toBeInTheDocument(); + }); + + it('handles changing fields', () => { + const { getByLabelText } = render(); + + const screenshots = getByLabelText('Screenshot options') as HTMLInputElement; + + fireEvent.change(screenshots, { target: { value: 'off' } }); + + expect(screenshots.value).toEqual('off'); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx new file mode 100644 index 00000000000000..28e2e39c79554a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiAccordion, + EuiSelect, + EuiFormRow, + EuiDescribedFormGroup, + EuiSpacer, +} from '@elastic/eui'; +import { ComboBox } from '../combo_box'; + +import { useBrowserAdvancedFieldsContext } from '../contexts'; + +import { ConfigKeys, ScreenshotOption } from '../types'; + +import { OptionalLabel } from '../optional_label'; + +export const BrowserAdvancedFields = () => { + const { fields, setFields } = useBrowserAdvancedFieldsContext(); + + const handleInputChange = useCallback( + ({ value, configKey }: { value: unknown; configKey: ConfigKeys }) => { + setFields((prevFields) => ({ ...prevFields, [configKey]: value })); + }, + [setFields] + ); + + return ( + + + + + + } + description={ + + } + > + + + } + labelAppend={} + helpText={ + + } + > + + handleInputChange({ + value: event.target.value, + configKey: ConfigKeys.SCREENSHOTS, + }) + } + data-test-subj="syntheticsBrowserScreenshots" + /> + + + } + labelAppend={} + helpText={ + + } + > + + handleInputChange({ value, configKey: ConfigKeys.SYNTHETICS_ARGS }) + } + data-test-subj="syntheticsBrowserSyntheticsArgs" + /> + + + + ); +}; + +const requestMethodOptions = Object.values(ScreenshotOption).map((option) => ({ + value: option, + text: option.replace(/-/g, ' '), +})); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts new file mode 100644 index 00000000000000..722b1625f023d3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BrowserFields, ConfigKeys } from '../types'; +import { Formatter, commonFormatters, arrayToJsonFormatter } from '../common/formatters'; + +export type BrowserFormatMap = Record; + +export const browserFormatters: BrowserFormatMap = { + [ConfigKeys.SOURCE_ZIP_URL]: null, + [ConfigKeys.SOURCE_ZIP_USERNAME]: null, + [ConfigKeys.SOURCE_ZIP_PASSWORD]: null, + [ConfigKeys.SOURCE_ZIP_FOLDER]: null, + [ConfigKeys.SOURCE_INLINE]: null, + [ConfigKeys.PARAMS]: null, + [ConfigKeys.SCREENSHOTS]: null, + [ConfigKeys.SYNTHETICS_ARGS]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.SYNTHETICS_ARGS]), + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts new file mode 100644 index 00000000000000..2b742a188782ad --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BrowserFields, ConfigKeys } from '../types'; +import { + Normalizer, + commonNormalizers, + getNormalizer, + getJsonToArrayOrObjectNormalizer, +} from '../common/normalizers'; + +import { defaultBrowserSimpleFields, defaultBrowserAdvancedFields } from '../contexts'; + +export type BrowserNormalizerMap = Record; + +const defaultBrowserFields = { + ...defaultBrowserSimpleFields, + ...defaultBrowserAdvancedFields, +}; + +export const getBrowserNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, defaultBrowserFields); +}; + +export const getBrowserJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { + return getJsonToArrayOrObjectNormalizer(key, defaultBrowserFields); +}; + +export const browserNormalizers: BrowserNormalizerMap = { + [ConfigKeys.SOURCE_ZIP_URL]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_URL), + [ConfigKeys.SOURCE_ZIP_USERNAME]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_USERNAME), + [ConfigKeys.SOURCE_ZIP_PASSWORD]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_PASSWORD), + [ConfigKeys.SOURCE_ZIP_FOLDER]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_FOLDER), + [ConfigKeys.SOURCE_INLINE]: getBrowserNormalizer(ConfigKeys.SOURCE_INLINE), + [ConfigKeys.PARAMS]: getBrowserNormalizer(ConfigKeys.PARAMS), + [ConfigKeys.SCREENSHOTS]: getBrowserNormalizer(ConfigKeys.SCREENSHOTS), + [ConfigKeys.SYNTHETICS_ARGS]: getBrowserJsonToArrayOrObjectNormalizer(ConfigKeys.SYNTHETICS_ARGS), + ...commonNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx new file mode 100644 index 00000000000000..34f56a65df3e8d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui'; +import { ConfigKeys, Validation } from '../types'; +import { useBrowserSimpleFieldsContext } from '../contexts'; +import { ComboBox } from '../combo_box'; +import { OptionalLabel } from '../optional_label'; +import { ScheduleField } from '../schedule_field'; +import { SourceField } from './source_field'; + +interface Props { + validate: Validation; +} + +export const BrowserSimpleFields = memo(({ validate }) => { + const { fields, setFields, defaultValues } = useBrowserSimpleFieldsContext(); + const handleInputChange = ({ value, configKey }: { value: unknown; configKey: ConfigKeys }) => { + setFields((prevFields) => ({ ...prevFields, [configKey]: value })); + }; + const onChangeSourceField = useCallback( + ({ zipUrl, folder, username, password, inlineScript, params }) => { + setFields((prevFields) => ({ + ...prevFields, + [ConfigKeys.SOURCE_ZIP_URL]: zipUrl, + [ConfigKeys.SOURCE_ZIP_FOLDER]: folder, + [ConfigKeys.SOURCE_ZIP_USERNAME]: username, + [ConfigKeys.SOURCE_ZIP_PASSWORD]: password, + [ConfigKeys.SOURCE_INLINE]: inlineScript, + [ConfigKeys.PARAMS]: params, + })); + }, + [setFields] + ); + + return ( + <> + + } + isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields)} + error={ + + } + > + + handleInputChange({ + value: schedule, + configKey: ConfigKeys.SCHEDULE, + }) + } + number={fields[ConfigKeys.SCHEDULE].number} + unit={fields[ConfigKeys.SCHEDULE].unit} + /> + + + } + > + ({ + zipUrl: defaultValues[ConfigKeys.SOURCE_ZIP_URL], + folder: defaultValues[ConfigKeys.SOURCE_ZIP_FOLDER], + username: defaultValues[ConfigKeys.SOURCE_ZIP_USERNAME], + password: defaultValues[ConfigKeys.SOURCE_ZIP_PASSWORD], + inlineScript: defaultValues[ConfigKeys.SOURCE_INLINE], + params: defaultValues[ConfigKeys.PARAMS], + }), + [defaultValues] + )} + /> + + + } + labelAppend={} + helpText={ + + } + > + + handleInputChange({ + value: event.target.value, + configKey: ConfigKeys.APM_SERVICE_NAME, + }) + } + data-test-subj="syntheticsAPMServiceName" + /> + + + } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} + error={ + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) + } + helpText={ + + } + > + + handleInputChange({ + value: event.target.value, + configKey: ConfigKeys.TIMEOUT, + }) + } + step={'any'} + /> + + + } + labelAppend={} + helpText={ + + } + > + handleInputChange({ value, configKey: ConfigKeys.TAGS })} + data-test-subj="syntheticsTags" + /> + + + ); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx new file mode 100644 index 00000000000000..eca354f30c973a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiTabbedContent, + EuiFormRow, + EuiFieldText, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; +import { OptionalLabel } from '../optional_label'; +import { CodeEditor } from '../code_editor'; +import { MonacoEditorLangId } from '../types'; + +enum SourceType { + INLINE = 'syntheticsBrowserInlineConfig', + ZIP = 'syntheticsBrowserZipURLConfig', +} + +interface SourceConfig { + zipUrl: string; + folder: string; + username: string; + password: string; + inlineScript: string; + params: string; +} + +interface Props { + onChange: (sourceConfig: SourceConfig) => void; + defaultConfig: SourceConfig; +} + +const defaultValues = { + zipUrl: '', + folder: '', + username: '', + password: '', + inlineScript: '', + params: '', +}; + +export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) => { + const [sourceType, setSourceType] = useState( + defaultConfig.inlineScript ? SourceType.INLINE : SourceType.ZIP + ); + const [config, setConfig] = useState(defaultConfig); + + useEffect(() => { + onChange(config); + }, [config, onChange]); + + const zipUrlLabel = ( + + ); + + const tabs = [ + { + id: 'syntheticsBrowserZipURLConfig', + name: zipUrlLabel, + content: ( + <> + + + } + helpText={ + + } + > + + setConfig((prevConfig) => ({ ...prevConfig, zipUrl: value })) + } + value={config.zipUrl} + /> + + + } + labelAppend={} + helpText={ + + } + > + + setConfig((prevConfig) => ({ ...prevConfig, folder: value })) + } + value={config.folder} + /> + + + } + labelAppend={} + helpText={ + + } + > + setConfig((prevConfig) => ({ ...prevConfig, params: code }))} + value={config.params} + /> + + + } + labelAppend={} + helpText={ + + } + > + + setConfig((prevConfig) => ({ ...prevConfig, username: value })) + } + value={config.username} + /> + + + } + labelAppend={} + helpText={ + + } + > + + setConfig((prevConfig) => ({ ...prevConfig, password: value })) + } + value={config.password} + /> + + + ), + }, + { + id: 'syntheticsBrowserInlineConfig', + name: ( + + ), + content: ( + + } + helpText={ + + } + > + setConfig((prevConfig) => ({ ...prevConfig, inlineScript: code }))} + value={config.inlineScript} + /> + + ), + }, + ]; + + return ( + tab.id === sourceType)} + autoFocus="selected" + onTabClick={(tab) => { + setSourceType(tab.id as SourceType); + if (tab.id !== sourceType) { + setConfig(defaultValues); + } + }} + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts new file mode 100644 index 00000000000000..bba8cefd749ee3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICommonFields, ConfigKeys, ScheduleUnit, DataStream } from '../types'; + +export const defaultValues: ICommonFields = { + [ConfigKeys.MONITOR_TYPE]: DataStream.HTTP, + [ConfigKeys.SCHEDULE]: { + number: '3', + unit: ScheduleUnit.MINUTES, + }, + [ConfigKeys.APM_SERVICE_NAME]: '', + [ConfigKeys.TAGS]: [], + [ConfigKeys.TIMEOUT]: '16', +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.test.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.test.ts new file mode 100644 index 00000000000000..9f4d8320e40482 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { arrayToJsonFormatter, objectToJsonFormatter, secondsToCronFormatter } from './formatters'; + +describe('formatters', () => { + describe('cronToSecondsNormalizer', () => { + it('takes a number of seconds and converts it to cron format', () => { + expect(secondsToCronFormatter('3')).toEqual('3s'); + }); + }); + + describe('arrayToJsonFormatter', () => { + it('takes an array and converts it to json', () => { + expect(arrayToJsonFormatter(['tag1', 'tag2'])).toEqual('["tag1","tag2"]'); + }); + + it('returns null if the array has length of 0', () => { + expect(arrayToJsonFormatter([])).toEqual(null); + }); + }); + + describe('objectToJsonFormatter', () => { + it('takes a json object string and returns an object', () => { + expect(objectToJsonFormatter({ key: 'value' })).toEqual('{"key":"value"}'); + }); + + it('returns null if the object has no keys', () => { + expect(objectToJsonFormatter({})).toEqual(null); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts new file mode 100644 index 00000000000000..311fa7da134984 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICommonFields, ICustomFields, ConfigKeys } from '../types'; + +export type Formatter = null | ((fields: Partial) => string | null); + +export type CommonFormatMap = Record; + +export const commonFormatters: CommonFormatMap = { + [ConfigKeys.NAME]: null, + [ConfigKeys.MONITOR_TYPE]: null, + [ConfigKeys.SCHEDULE]: (fields) => + JSON.stringify( + `@every ${fields[ConfigKeys.SCHEDULE]?.number}${fields[ConfigKeys.SCHEDULE]?.unit}` + ), + [ConfigKeys.APM_SERVICE_NAME]: null, + [ConfigKeys.TAGS]: (fields) => arrayToJsonFormatter(fields[ConfigKeys.TAGS]), + [ConfigKeys.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKeys.TIMEOUT]), +}; + +export const arrayToJsonFormatter = (value: string[] = []) => + value.length ? JSON.stringify(value) : null; + +export const secondsToCronFormatter = (value: string = '') => (value ? `${value}s` : null); + +export const objectToJsonFormatter = (value: Record = {}) => + Object.keys(value).length ? JSON.stringify(value) : null; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts new file mode 100644 index 00000000000000..055e829858a16a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cronToSecondsNormalizer, jsonToArrayOrObjectNormalizer } from './normalizers'; + +describe('normalizers', () => { + describe('cronToSecondsNormalizer', () => { + it('returns number of seconds from cron formatted seconds', () => { + expect(cronToSecondsNormalizer('3s')).toEqual('3'); + }); + }); + + describe('jsonToArrayOrObjectNormalizer', () => { + it('takes a json object string and returns an object', () => { + expect(jsonToArrayOrObjectNormalizer('{\n "key": "value"\n}')).toEqual({ + key: 'value', + }); + }); + + it('takes a json array string and returns an array', () => { + expect(jsonToArrayOrObjectNormalizer('["tag1","tag2"]')).toEqual(['tag1', 'tag2']); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts new file mode 100644 index 00000000000000..69121ca4bd70e2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICommonFields, ConfigKeys } from '../types'; +import { NewPackagePolicyInput } from '../../../../../fleet/common'; +import { defaultValues as commonDefaultValues } from './default_values'; + +// TO DO: create a standard input format that all fields resolve to +export type Normalizer = (fields: NewPackagePolicyInput['vars']) => unknown; + +// create a type of all the common policy fields, as well as the fleet managed 'name' field +export type CommonNormalizerMap = Record; + +/** + * Takes a cron formatted seconds and returns just the number of seconds. Assumes that cron is already in seconds format. + * @params {string} value (Ex '3s') + * @return {string} (Ex '3') + */ +export const cronToSecondsNormalizer = (value: string) => + value ? value.slice(0, value.length - 1) : null; + +export const jsonToArrayOrObjectNormalizer = (value: string) => (value ? JSON.parse(value) : null); + +export function getNormalizer(key: string, defaultValues: Fields): Normalizer { + return (fields: NewPackagePolicyInput['vars']) => + fields?.[key]?.value ?? defaultValues[key as keyof Fields]; +} + +export function getJsonToArrayOrObjectNormalizer( + key: string, + defaultValues: Fields +): Normalizer { + return (fields: NewPackagePolicyInput['vars']) => + jsonToArrayOrObjectNormalizer(fields?.[key]?.value) ?? defaultValues[key as keyof Fields]; +} + +export function getCronNormalizer(key: string, defaultValues: Fields): Normalizer { + return (fields: NewPackagePolicyInput['vars']) => + cronToSecondsNormalizer(fields?.[key]?.value) ?? defaultValues[key as keyof Fields]; +} + +export const getCommonNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, commonDefaultValues); +}; + +export const getCommonJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { + return getJsonToArrayOrObjectNormalizer(key, commonDefaultValues); +}; + +export const getCommonCronToSecondsNormalizer = (key: ConfigKeys) => { + return getCronNormalizer(key, commonDefaultValues); +}; + +export const commonNormalizers: CommonNormalizerMap = { + [ConfigKeys.NAME]: (fields) => fields?.[ConfigKeys.NAME]?.value ?? '', + [ConfigKeys.MONITOR_TYPE]: getCommonNormalizer(ConfigKeys.MONITOR_TYPE), + [ConfigKeys.SCHEDULE]: (fields) => { + const value = fields?.[ConfigKeys.SCHEDULE]?.value; + if (value) { + const fullString = JSON.parse(fields?.[ConfigKeys.SCHEDULE]?.value); + const fullSchedule = fullString.replace('@every ', ''); + const unit = fullSchedule.slice(-1); + const number = fullSchedule.slice(0, fullSchedule.length - 1); + return { + unit, + number, + }; + } else { + return commonDefaultValues[ConfigKeys.SCHEDULE]; + } + }, + [ConfigKeys.APM_SERVICE_NAME]: getCommonNormalizer(ConfigKeys.APM_SERVICE_NAME), + [ConfigKeys.TAGS]: getCommonJsonToArrayOrObjectNormalizer(ConfigKeys.TAGS), + [ConfigKeys.TIMEOUT]: getCommonCronToSecondsNormalizer(ConfigKeys.TIMEOUT), +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx index b51aa6cbf3a7cb..11796050a545b8 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx @@ -25,7 +25,7 @@ interface IHTTPAdvancedFieldsContextProvider { defaultValues?: IHTTPAdvancedFields; } -export const initialValues = { +export const initialValues: IHTTPAdvancedFields = { [ConfigKeys.PASSWORD]: '', [ConfigKeys.PROXY_URL]: '', [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: [], diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx index 6e4f46111c283f..ef821b7e39dcaf 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx @@ -19,7 +19,7 @@ interface ITCPAdvancedFieldsContextProvider { defaultValues?: ITCPAdvancedFields; } -export const initialValues = { +export const initialValues: ITCPAdvancedFields = { [ConfigKeys.PROXY_URL]: '', [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: false, [ConfigKeys.RESPONSE_RECEIVE_CHECK]: '', diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx new file mode 100644 index 00000000000000..1d1493178b944f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { IBrowserSimpleFields, ConfigKeys, DataStream } from '../types'; +import { defaultValues as commonDefaultValues } from '../common/default_values'; + +interface IBrowserSimpleFieldsContext { + setFields: React.Dispatch>; + fields: IBrowserSimpleFields; + defaultValues: IBrowserSimpleFields; +} + +interface IBrowserSimpleFieldsContextProvider { + children: React.ReactNode; + defaultValues?: IBrowserSimpleFields; +} + +export const initialValues: IBrowserSimpleFields = { + ...commonDefaultValues, + [ConfigKeys.MONITOR_TYPE]: DataStream.BROWSER, + [ConfigKeys.SOURCE_ZIP_URL]: '', + [ConfigKeys.SOURCE_ZIP_USERNAME]: '', + [ConfigKeys.SOURCE_ZIP_PASSWORD]: '', + [ConfigKeys.SOURCE_ZIP_FOLDER]: '', + [ConfigKeys.SOURCE_INLINE]: '', + [ConfigKeys.PARAMS]: '', +}; + +const defaultContext: IBrowserSimpleFieldsContext = { + setFields: (_fields: React.SetStateAction) => { + throw new Error( + 'setFields was not initialized for Browser Simple Fields, set it when you invoke the context' + ); + }, + fields: initialValues, // mutable + defaultValues: initialValues, // immutable +}; + +export const BrowserSimpleFieldsContext = createContext(defaultContext); + +export const BrowserSimpleFieldsContextProvider = ({ + children, + defaultValues = initialValues, +}: IBrowserSimpleFieldsContextProvider) => { + const [fields, setFields] = useState(defaultValues); + + const value = useMemo(() => { + return { fields, setFields, defaultValues }; + }, [fields, defaultValues]); + + return ; +}; + +export const useBrowserSimpleFieldsContext = () => useContext(BrowserSimpleFieldsContext); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx new file mode 100644 index 00000000000000..3f3bb8f14c269c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { IBrowserAdvancedFields, ConfigKeys, ScreenshotOption } from '../types'; + +interface IBrowserAdvancedFieldsContext { + setFields: React.Dispatch>; + fields: IBrowserAdvancedFields; + defaultValues: IBrowserAdvancedFields; +} + +interface IBrowserAdvancedFieldsContextProvider { + children: React.ReactNode; + defaultValues?: IBrowserAdvancedFields; +} + +export const initialValues: IBrowserAdvancedFields = { + [ConfigKeys.SCREENSHOTS]: ScreenshotOption.ON, + [ConfigKeys.SYNTHETICS_ARGS]: [], +}; + +const defaultContext: IBrowserAdvancedFieldsContext = { + setFields: (_fields: React.SetStateAction) => { + throw new Error( + 'setFields was not initialized for Browser Advanced Fields, set it when you invoke the context' + ); + }, + fields: initialValues, // mutable + defaultValues: initialValues, // immutable +}; + +export const BrowserAdvancedFieldsContext = createContext(defaultContext); + +export const BrowserAdvancedFieldsContextProvider = ({ + children, + defaultValues = initialValues, +}: IBrowserAdvancedFieldsContextProvider) => { + const [fields, setFields] = useState(defaultValues); + + const value = useMemo(() => { + return { fields, setFields, defaultValues }; + }, [fields, defaultValues]); + + return ; +}; + +export const useBrowserAdvancedFieldsContext = () => useContext(BrowserAdvancedFieldsContext); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_provider.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_provider.tsx new file mode 100644 index 00000000000000..e2ce88f84f7022 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_provider.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode } from 'react'; +import { BrowserFields, IBrowserSimpleFields, IBrowserAdvancedFields } from '../types'; +import { + BrowserSimpleFieldsContextProvider, + BrowserAdvancedFieldsContextProvider, + defaultBrowserSimpleFields, + defaultBrowserAdvancedFields, +} from '.'; +import { formatDefaultValues } from '../helpers/context_helpers'; + +interface BrowserContextProviderProps { + defaultValues?: BrowserFields; + children: ReactNode; +} + +export const BrowserContextProvider = ({ + defaultValues, + children, +}: BrowserContextProviderProps) => { + const simpleKeys = Object.keys(defaultBrowserSimpleFields) as Array; + const advancedKeys = Object.keys(defaultBrowserAdvancedFields) as Array< + keyof IBrowserAdvancedFields + >; + const formattedDefaultSimpleFields = formatDefaultValues( + simpleKeys, + defaultValues || {} + ); + const formattedDefaultAdvancedFields = formatDefaultValues( + advancedKeys, + defaultValues || {} + ); + const simpleFields: IBrowserSimpleFields | undefined = defaultValues + ? formattedDefaultSimpleFields + : undefined; + const advancedFields: IBrowserAdvancedFields | undefined = defaultValues + ? formattedDefaultAdvancedFields + : undefined; + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context.tsx index d1306836afa9c8..d8b89a1dfc4d09 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context.tsx @@ -6,7 +6,8 @@ */ import React, { createContext, useContext, useMemo, useState } from 'react'; -import { IHTTPSimpleFields, ConfigKeys, ScheduleUnit, DataStream } from '../types'; +import { IHTTPSimpleFields, ConfigKeys, DataStream } from '../types'; +import { defaultValues as commonDefaultValues } from '../common/default_values'; interface IHTTPSimpleFieldsContext { setFields: React.Dispatch>; @@ -19,17 +20,11 @@ interface IHTTPSimpleFieldsContextProvider { defaultValues?: IHTTPSimpleFields; } -export const initialValues = { +export const initialValues: IHTTPSimpleFields = { + ...commonDefaultValues, [ConfigKeys.URLS]: '', [ConfigKeys.MAX_REDIRECTS]: '0', [ConfigKeys.MONITOR_TYPE]: DataStream.HTTP, - [ConfigKeys.SCHEDULE]: { - number: '3', - unit: ScheduleUnit.MINUTES, - }, - [ConfigKeys.APM_SERVICE_NAME]: '', - [ConfigKeys.TAGS]: [], - [ConfigKeys.TIMEOUT]: '16', }; const defaultContext: IHTTPSimpleFieldsContext = { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_provider.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_provider.tsx index e48de76862e24a..ea577f33369360 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_provider.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_provider.tsx @@ -6,63 +6,39 @@ */ import React, { ReactNode } from 'react'; -import { IHTTPSimpleFields, IHTTPAdvancedFields, ITLSFields, ConfigKeys } from '../types'; +import { HTTPFields, IHTTPSimpleFields, IHTTPAdvancedFields } from '../types'; import { HTTPSimpleFieldsContextProvider, HTTPAdvancedFieldsContextProvider, - TLSFieldsContextProvider, + defaultHTTPSimpleFields, + defaultHTTPAdvancedFields, } from '.'; +import { formatDefaultValues } from '../helpers/context_helpers'; interface HTTPContextProviderProps { - defaultValues?: any; + defaultValues?: HTTPFields; children: ReactNode; } export const HTTPContextProvider = ({ defaultValues, children }: HTTPContextProviderProps) => { - const httpAdvancedFields: IHTTPAdvancedFields | undefined = defaultValues - ? { - [ConfigKeys.USERNAME]: defaultValues[ConfigKeys.USERNAME], - [ConfigKeys.PASSWORD]: defaultValues[ConfigKeys.PASSWORD], - [ConfigKeys.PROXY_URL]: defaultValues[ConfigKeys.PROXY_URL], - [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: - defaultValues[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE], - [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: - defaultValues[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE], - [ConfigKeys.RESPONSE_BODY_INDEX]: defaultValues[ConfigKeys.RESPONSE_BODY_INDEX], - [ConfigKeys.RESPONSE_HEADERS_CHECK]: defaultValues[ConfigKeys.RESPONSE_HEADERS_CHECK], - [ConfigKeys.RESPONSE_HEADERS_INDEX]: defaultValues[ConfigKeys.RESPONSE_HEADERS_INDEX], - [ConfigKeys.RESPONSE_STATUS_CHECK]: defaultValues[ConfigKeys.RESPONSE_STATUS_CHECK], - [ConfigKeys.REQUEST_BODY_CHECK]: defaultValues[ConfigKeys.REQUEST_BODY_CHECK], - [ConfigKeys.REQUEST_HEADERS_CHECK]: defaultValues[ConfigKeys.REQUEST_HEADERS_CHECK], - [ConfigKeys.REQUEST_METHOD_CHECK]: defaultValues[ConfigKeys.REQUEST_METHOD_CHECK], - } - : undefined; + const simpleKeys = Object.keys(defaultHTTPSimpleFields) as Array; + const advancedKeys = Object.keys(defaultHTTPAdvancedFields) as Array; + const formattedDefaultHTTPSimpleFields = formatDefaultValues( + simpleKeys, + defaultValues || {} + ); + const formattedDefaultHTTPAdvancedFields = formatDefaultValues( + advancedKeys, + defaultValues || {} + ); + const httpAdvancedFields = defaultValues ? formattedDefaultHTTPAdvancedFields : undefined; const httpSimpleFields: IHTTPSimpleFields | undefined = defaultValues - ? { - [ConfigKeys.APM_SERVICE_NAME]: defaultValues[ConfigKeys.APM_SERVICE_NAME], - [ConfigKeys.MAX_REDIRECTS]: defaultValues[ConfigKeys.MAX_REDIRECTS], - [ConfigKeys.MONITOR_TYPE]: defaultValues[ConfigKeys.MONITOR_TYPE], - [ConfigKeys.SCHEDULE]: defaultValues[ConfigKeys.SCHEDULE], - [ConfigKeys.TAGS]: defaultValues[ConfigKeys.TAGS], - [ConfigKeys.TIMEOUT]: defaultValues[ConfigKeys.TIMEOUT], - [ConfigKeys.URLS]: defaultValues[ConfigKeys.URLS], - } - : undefined; - const tlsFields: ITLSFields | undefined = defaultValues - ? { - [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: - defaultValues[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES], - [ConfigKeys.TLS_CERTIFICATE]: defaultValues[ConfigKeys.TLS_CERTIFICATE], - [ConfigKeys.TLS_KEY]: defaultValues[ConfigKeys.TLS_KEY], - [ConfigKeys.TLS_KEY_PASSPHRASE]: defaultValues[ConfigKeys.TLS_KEY_PASSPHRASE], - [ConfigKeys.TLS_VERIFICATION_MODE]: defaultValues[ConfigKeys.TLS_VERIFICATION_MODE], - [ConfigKeys.TLS_VERSION]: defaultValues[ConfigKeys.TLS_VERSION], - } + ? formattedDefaultHTTPSimpleFields : undefined; return ( - {children} + {children} ); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/icmp_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/icmp_context.tsx index 93c67c6133ce9f..eb7227ebceb07b 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/icmp_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/icmp_context.tsx @@ -6,7 +6,8 @@ */ import React, { createContext, useContext, useMemo, useState } from 'react'; -import { IICMPSimpleFields, ConfigKeys, ScheduleUnit, DataStream } from '../types'; +import { IICMPSimpleFields, ConfigKeys, DataStream } from '../types'; +import { defaultValues as commonDefaultValues } from '../common/default_values'; interface IICMPSimpleFieldsContext { setFields: React.Dispatch>; @@ -19,17 +20,10 @@ interface IICMPSimpleFieldsContextProvider { defaultValues?: IICMPSimpleFields; } -export const initialValues = { +export const initialValues: IICMPSimpleFields = { + ...commonDefaultValues, [ConfigKeys.HOSTS]: '', - [ConfigKeys.MAX_REDIRECTS]: '0', [ConfigKeys.MONITOR_TYPE]: DataStream.ICMP, - [ConfigKeys.SCHEDULE]: { - number: '3', - unit: ScheduleUnit.MINUTES, - }, - [ConfigKeys.APM_SERVICE_NAME]: '', - [ConfigKeys.TAGS]: [], - [ConfigKeys.TIMEOUT]: '16', [ConfigKeys.WAIT]: '1', }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts index f84a4e75df922a..e955d2d7d4d50a 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - export { MonitorTypeContext, MonitorTypeContextProvider, @@ -17,6 +16,12 @@ export { initialValues as defaultHTTPSimpleFields, useHTTPSimpleFieldsContext, } from './http_context'; +export { + HTTPAdvancedFieldsContext, + HTTPAdvancedFieldsContextProvider, + initialValues as defaultHTTPAdvancedFields, + useHTTPAdvancedFieldsContext, +} from './advanced_fields_http_context'; export { TCPSimpleFieldsContext, TCPSimpleFieldsContextProvider, @@ -36,11 +41,17 @@ export { useTCPAdvancedFieldsContext, } from './advanced_fields_tcp_context'; export { - HTTPAdvancedFieldsContext, - HTTPAdvancedFieldsContextProvider, - initialValues as defaultHTTPAdvancedFields, - useHTTPAdvancedFieldsContext, -} from './advanced_fields_http_context'; + BrowserSimpleFieldsContext, + BrowserSimpleFieldsContextProvider, + initialValues as defaultBrowserSimpleFields, + useBrowserSimpleFieldsContext, +} from './browser_context'; +export { + BrowserAdvancedFieldsContext, + BrowserAdvancedFieldsContextProvider, + initialValues as defaultBrowserAdvancedFields, + useBrowserAdvancedFieldsContext, +} from './browser_context_advanced'; export { TLSFieldsContext, TLSFieldsContextProvider, @@ -49,3 +60,4 @@ export { } from './tls_fields_context'; export { HTTPContextProvider } from './http_provider'; export { TCPContextProvider } from './tcp_provider'; +export { BrowserContextProvider } from './browser_provider'; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context.tsx index 6020a7ff2bff8d..a1e01cb7faab72 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context.tsx @@ -6,7 +6,8 @@ */ import React, { createContext, useContext, useMemo, useState } from 'react'; -import { ITCPSimpleFields, ConfigKeys, ScheduleUnit, DataStream } from '../types'; +import { ITCPSimpleFields, ConfigKeys, DataStream } from '../types'; +import { defaultValues as commonDefaultValues } from '../common/default_values'; interface ITCPSimpleFieldsContext { setFields: React.Dispatch>; @@ -19,17 +20,10 @@ interface ITCPSimpleFieldsContextProvider { defaultValues?: ITCPSimpleFields; } -export const initialValues = { +export const initialValues: ITCPSimpleFields = { + ...commonDefaultValues, [ConfigKeys.HOSTS]: '', - [ConfigKeys.MAX_REDIRECTS]: '0', [ConfigKeys.MONITOR_TYPE]: DataStream.TCP, - [ConfigKeys.SCHEDULE]: { - number: '3', - unit: ScheduleUnit.MINUTES, - }, - [ConfigKeys.APM_SERVICE_NAME]: '', - [ConfigKeys.TAGS]: [], - [ConfigKeys.TIMEOUT]: '16', }; const defaultContext: ITCPSimpleFieldsContext = { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_provider.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_provider.tsx index 666839803f4d67..b62e87a566b97c 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_provider.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_provider.tsx @@ -6,56 +6,41 @@ */ import React, { ReactNode } from 'react'; -import { ConfigKeys, ITCPSimpleFields, ITCPAdvancedFields, ITLSFields } from '../types'; +import { TCPFields, ITCPSimpleFields, ITCPAdvancedFields } from '../types'; import { TCPSimpleFieldsContextProvider, TCPAdvancedFieldsContextProvider, - TLSFieldsContextProvider, + defaultTCPSimpleFields, + defaultTCPAdvancedFields, } from '.'; +import { formatDefaultValues } from '../helpers/context_helpers'; interface TCPContextProviderProps { - defaultValues?: any; + defaultValues?: TCPFields; children: ReactNode; } -/** - * Exports Synthetics-specific package policy instructions - * for use in the Ingest app create / edit package policy - */ export const TCPContextProvider = ({ defaultValues, children }: TCPContextProviderProps) => { - const tcpSimpleFields: ITCPSimpleFields | undefined = defaultValues - ? { - [ConfigKeys.APM_SERVICE_NAME]: defaultValues[ConfigKeys.APM_SERVICE_NAME], - [ConfigKeys.HOSTS]: defaultValues[ConfigKeys.HOSTS], - [ConfigKeys.MONITOR_TYPE]: defaultValues[ConfigKeys.MONITOR_TYPE], - [ConfigKeys.SCHEDULE]: defaultValues[ConfigKeys.SCHEDULE], - [ConfigKeys.TAGS]: defaultValues[ConfigKeys.TAGS], - [ConfigKeys.TIMEOUT]: defaultValues[ConfigKeys.TIMEOUT], - } - : undefined; - const tcpAdvancedFields: ITCPAdvancedFields | undefined = defaultValues - ? { - [ConfigKeys.PROXY_URL]: defaultValues[ConfigKeys.PROXY_URL], - [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: defaultValues[ConfigKeys.PROXY_USE_LOCAL_RESOLVER], - [ConfigKeys.RESPONSE_RECEIVE_CHECK]: defaultValues[ConfigKeys.RESPONSE_RECEIVE_CHECK], - [ConfigKeys.REQUEST_SEND_CHECK]: defaultValues[ConfigKeys.REQUEST_SEND_CHECK], - } + const simpleKeys = Object.keys(defaultTCPSimpleFields) as Array; + const advancedKeys = Object.keys(defaultTCPAdvancedFields) as Array; + const formattedDefaultSimpleFields = formatDefaultValues( + simpleKeys, + defaultValues || {} + ); + const formattedDefaultAdvancedFields = formatDefaultValues( + advancedKeys, + defaultValues || {} + ); + const simpleFields: ITCPSimpleFields | undefined = defaultValues + ? formattedDefaultSimpleFields : undefined; - const tlsFields: ITLSFields | undefined = defaultValues - ? { - [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: - defaultValues[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES], - [ConfigKeys.TLS_CERTIFICATE]: defaultValues[ConfigKeys.TLS_CERTIFICATE], - [ConfigKeys.TLS_KEY]: defaultValues[ConfigKeys.TLS_KEY], - [ConfigKeys.TLS_KEY_PASSPHRASE]: defaultValues[ConfigKeys.TLS_KEY_PASSPHRASE], - [ConfigKeys.TLS_VERIFICATION_MODE]: defaultValues[ConfigKeys.TLS_VERIFICATION_MODE], - [ConfigKeys.TLS_VERSION]: defaultValues[ConfigKeys.TLS_VERSION], - } + const advancedFields: ITCPAdvancedFields | undefined = defaultValues + ? formattedDefaultAdvancedFields : undefined; return ( - - - {children} + + + {children} ); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tls_fields_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tls_fields_context.tsx index eaeb9956544487..2a88b8c88e96cc 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/tls_fields_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tls_fields_context.tsx @@ -19,7 +19,7 @@ interface ITLSFieldsContextProvider { defaultValues?: ITLSFields; } -export const initialValues = { +export const initialValues: ITLSFields = { [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: { value: '', isEnabled: false, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx index e114ea72b8f49c..5bcd235b9b60ed 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import 'jest-canvas-mock'; import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; @@ -11,8 +12,10 @@ import { render } from '../../lib/helper/rtl_helpers'; import { TCPContextProvider, HTTPContextProvider, + BrowserContextProvider, ICMPSimpleFieldsContextProvider, MonitorTypeContextProvider, + TLSFieldsContextProvider, } from './contexts'; import { CustomFields } from './custom_fields'; import { ConfigKeys, DataStream, ScheduleUnit } from './types'; @@ -30,14 +33,26 @@ const defaultHTTPConfig = defaultConfig[DataStream.HTTP]; const defaultTCPConfig = defaultConfig[DataStream.TCP]; describe('', () => { - const WrappedComponent = ({ validate = defaultValidation, typeEditable = false }) => { + const WrappedComponent = ({ + validate = defaultValidation, + typeEditable = false, + dataStreams = [DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER], + }) => { return ( - - - + + + + + + + @@ -149,7 +164,7 @@ describe('', () => { }); it('handles switching monitor type', () => { - const { getByText, getByLabelText, queryByLabelText } = render( + const { getByText, getByLabelText, queryByLabelText, getAllByLabelText } = render( ); const monitorType = getByLabelText('Monitor Type') as HTMLInputElement; @@ -168,7 +183,7 @@ describe('', () => { expect(queryByLabelText('Max redirects')).not.toBeInTheDocument(); // ensure at least one tcp advanced option is present - const advancedOptionsButton = getByText('Advanced TCP options'); + let advancedOptionsButton = getByText('Advanced TCP options'); fireEvent.click(advancedOptionsButton); expect(queryByLabelText('Request method')).not.toBeInTheDocument(); @@ -181,6 +196,21 @@ describe('', () => { // expect TCP fields not to be in the DOM expect(queryByLabelText('Proxy URL')).not.toBeInTheDocument(); + + fireEvent.change(monitorType, { target: { value: DataStream.BROWSER } }); + + // expect browser fields to be in the DOM + getAllByLabelText('Zip URL').forEach((node) => { + expect(node).toBeInTheDocument(); + }); + + // ensure at least one browser advanced option is present + advancedOptionsButton = getByText('Advanced Browser options'); + fireEvent.click(advancedOptionsButton); + expect(getByLabelText('Screenshot options')).toBeInTheDocument(); + + // expect ICMP fields not to be in the DOM + expect(queryByLabelText('Wait in seconds')).not.toBeInTheDocument(); }); it('shows resolve hostnames locally field when proxy url is filled for tcp monitors', () => { @@ -213,7 +243,7 @@ describe('', () => { const urlError = getByText('URL is required'); const monitorIntervalError = getByText('Monitor interval is required'); const maxRedirectsError = getByText('Max redirects must be 0 or greater'); - const timeoutError = getByText('Timeout must be 0 or greater and less than schedule interval'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(urlError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -229,16 +259,35 @@ describe('', () => { expect(queryByText('URL is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); // create more errors fireEvent.change(monitorIntervalNumber, { target: { value: '1' } }); // 1 minute - fireEvent.change(timeout, { target: { value: '61' } }); // timeout cannot be more than monitor interval + fireEvent.change(timeout, { target: { value: '611' } }); // timeout cannot be more than monitor interval - const timeoutError2 = getByText('Timeout must be 0 or greater and less than schedule interval'); + const timeoutError2 = getByText('Timeout must be less than the monitor interval'); expect(timeoutError2).toBeInTheDocument(); }); + + it('does not show monitor options that are not contained in datastreams', async () => { + const { getByText, queryByText, queryByLabelText } = render( + + ); + + const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement; + + // resolve errors + fireEvent.click(monitorType); + + waitFor(() => { + expect(getByText('http')).toBeInTheDocument(); + expect(getByText('tcp')).toBeInTheDocument(); + expect(getByText('icmp')).toBeInTheDocument(); + expect(queryByText('browser')).not.toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index 0d9291261b82d6..87f7a98aa4a6fb 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import React, { useState, memo } from 'react'; +import React, { useState, useMemo, memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, @@ -25,20 +24,39 @@ import { HTTPAdvancedFields } from './http/advanced_fields'; import { TCPSimpleFields } from './tcp/simple_fields'; import { TCPAdvancedFields } from './tcp/advanced_fields'; import { ICMPSimpleFields } from './icmp/simple_fields'; +import { BrowserSimpleFields } from './browser/simple_fields'; +import { BrowserAdvancedFields } from './browser/advanced_fields'; interface Props { typeEditable?: boolean; isTLSEnabled?: boolean; validate: Validation; + dataStreams?: DataStream[]; } export const CustomFields = memo( - ({ typeEditable = false, isTLSEnabled: defaultIsTLSEnabled = false, validate }) => { + ({ + typeEditable = false, + isTLSEnabled: defaultIsTLSEnabled = false, + validate, + dataStreams = [], + }) => { const [isTLSEnabled, setIsTLSEnabled] = useState(defaultIsTLSEnabled); const { monitorType, setMonitorType } = useMonitorTypeContext(); const isHTTP = monitorType === DataStream.HTTP; const isTCP = monitorType === DataStream.TCP; + const isBrowser = monitorType === DataStream.BROWSER; + + const dataStreamOptions = useMemo(() => { + const dataStreamToString = [ + { value: DataStream.HTTP, text: 'HTTP' }, + { value: DataStream.TCP, text: 'TCP' }, + { value: DataStream.ICMP, text: 'ICMP' }, + { value: DataStream.BROWSER, text: 'Browser' }, + ]; + return dataStreamToString.filter((dataStream) => dataStreams.includes(dataStream.value)); + }, [dataStreams]); const renderSimpleFields = (type: DataStream) => { switch (type) { @@ -48,6 +66,8 @@ export const CustomFields = memo( return ; case DataStream.TCP: return ; + case DataStream.BROWSER: + return ; default: return null; } @@ -82,7 +102,11 @@ export const CustomFields = memo( defaultMessage="Monitor Type" /> } - isInvalid={!!validate[ConfigKeys.MONITOR_TYPE]?.(monitorType)} + isInvalid={ + !!validate[ConfigKeys.MONITOR_TYPE]?.({ + [ConfigKeys.MONITOR_TYPE]: monitorType, + }) + } error={ ( {isHTTP && } {isTCP && } + {isBrowser && } ); } ); - -const dataStreamOptions = [ - { value: DataStream.HTTP, text: 'HTTP' }, - { value: DataStream.TCP, text: 'TCP' }, - { value: DataStream.ICMP, text: 'ICMP' }, -]; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/helpers/context_helpers.ts b/x-pack/plugins/uptime/public/components/fleet_package/helpers/context_helpers.ts new file mode 100644 index 00000000000000..acd8bdf95ce858 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/helpers/context_helpers.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function formatDefaultValues( + keys: Array, + defaultValues: Partial +) { + return keys.reduce((acc: any, currentValue) => { + const key = currentValue as keyof Fields; + acc[key] = defaultValues?.[key]; + return acc; + }, {}) as Fields; +} diff --git a/x-pack/plugins/uptime/public/components/fleet_package/helpers/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/helpers/formatters.ts new file mode 100644 index 00000000000000..8ca10516a62004 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/helpers/formatters.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataStream } from '../types'; + +import { httpFormatters, HTTPFormatMap } from '../http/formatters'; +import { tcpFormatters, TCPFormatMap } from '../tcp/formatters'; +import { icmpFormatters, ICMPFormatMap } from '../icmp/formatters'; +import { browserFormatters, BrowserFormatMap } from '../browser/formatters'; +import { commonFormatters, CommonFormatMap } from '../common/formatters'; + +type Formatters = HTTPFormatMap & TCPFormatMap & ICMPFormatMap & BrowserFormatMap & CommonFormatMap; + +interface FormatterMap { + [DataStream.HTTP]: HTTPFormatMap; + [DataStream.ICMP]: ICMPFormatMap; + [DataStream.TCP]: TCPFormatMap; + [DataStream.BROWSER]: BrowserFormatMap; +} + +export const formattersMap: FormatterMap = { + [DataStream.HTTP]: httpFormatters, + [DataStream.ICMP]: icmpFormatters, + [DataStream.TCP]: tcpFormatters, + [DataStream.BROWSER]: browserFormatters, +}; + +export const formatters: Formatters = { + ...httpFormatters, + ...icmpFormatters, + ...tcpFormatters, + ...browserFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/helpers/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/helpers/normalizers.ts new file mode 100644 index 00000000000000..60aa607aebe61b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/helpers/normalizers.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataStream } from '../types'; + +import { httpNormalizers, HTTPNormalizerMap } from '../http/normalizers'; +import { tcpNormalizers, TCPNormalizerMap } from '../tcp/normalizers'; +import { icmpNormalizers, ICMPNormalizerMap } from '../icmp/normalizers'; +import { browserNormalizers, BrowserNormalizerMap } from '../browser/normalizers'; +import { commonNormalizers, CommonNormalizerMap } from '../common/normalizers'; + +type Normalizers = HTTPNormalizerMap & + ICMPNormalizerMap & + TCPNormalizerMap & + BrowserNormalizerMap & + CommonNormalizerMap; + +interface NormalizerMap { + [DataStream.HTTP]: HTTPNormalizerMap; + [DataStream.ICMP]: ICMPNormalizerMap; + [DataStream.TCP]: TCPNormalizerMap; + [DataStream.BROWSER]: BrowserNormalizerMap; +} + +export const normalizersMap: NormalizerMap = { + [DataStream.HTTP]: httpNormalizers, + [DataStream.ICMP]: icmpNormalizers, + [DataStream.TCP]: tcpNormalizers, + [DataStream.BROWSER]: browserNormalizers, +}; + +export const normalizers: Normalizers = { + ...httpNormalizers, + ...icmpNormalizers, + ...tcpNormalizers, + ...browserNormalizers, + ...commonNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx index 267ccd678ddadc..c38ac509e377e3 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/advanced_fields.tsx @@ -186,18 +186,12 @@ export const HTTPAdvancedFields = memo(({ validate }) => { /> } labelAppend={} - isInvalid={ - !!validate[ConfigKeys.REQUEST_HEADERS_CHECK]?.(fields[ConfigKeys.REQUEST_HEADERS_CHECK]) - } + isInvalid={!!validate[ConfigKeys.REQUEST_HEADERS_CHECK]?.(fields)} error={ - !!validate[ConfigKeys.REQUEST_HEADERS_CHECK]?.( - fields[ConfigKeys.REQUEST_HEADERS_CHECK] - ) ? ( - - ) : undefined + } helpText={ (({ validate }) => { /> } labelAppend={} - isInvalid={ - !!validate[ConfigKeys.RESPONSE_STATUS_CHECK]?.(fields[ConfigKeys.RESPONSE_STATUS_CHECK]) - } + isInvalid={!!validate[ConfigKeys.RESPONSE_STATUS_CHECK]?.(fields)} error={ (({ validate }) => { /> } labelAppend={} - isInvalid={ - !!validate[ConfigKeys.RESPONSE_HEADERS_CHECK]?.( - fields[ConfigKeys.RESPONSE_HEADERS_CHECK] - ) - } - error={ - !!validate[ConfigKeys.RESPONSE_HEADERS_CHECK]?.( - fields[ConfigKeys.RESPONSE_HEADERS_CHECK] - ) - ? [ - , - ] - : undefined - } + isInvalid={!!validate[ConfigKeys.RESPONSE_HEADERS_CHECK]?.(fields)} + error={[ + , + ]} helpText={ ; + +export const httpFormatters: HTTPFormatMap = { + [ConfigKeys.URLS]: null, + [ConfigKeys.MAX_REDIRECTS]: null, + [ConfigKeys.USERNAME]: null, + [ConfigKeys.PASSWORD]: null, + [ConfigKeys.PROXY_URL]: null, + [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]), + [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]), + [ConfigKeys.RESPONSE_BODY_INDEX]: null, + [ConfigKeys.RESPONSE_HEADERS_CHECK]: (fields) => + objectToJsonFormatter(fields[ConfigKeys.RESPONSE_HEADERS_CHECK]), + [ConfigKeys.RESPONSE_HEADERS_INDEX]: null, + [ConfigKeys.RESPONSE_STATUS_CHECK]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.RESPONSE_STATUS_CHECK]), + [ConfigKeys.REQUEST_BODY_CHECK]: (fields) => + fields[ConfigKeys.REQUEST_BODY_CHECK]?.value + ? JSON.stringify(fields[ConfigKeys.REQUEST_BODY_CHECK]?.value) + : null, + [ConfigKeys.REQUEST_HEADERS_CHECK]: (fields) => + objectToJsonFormatter(fields[ConfigKeys.REQUEST_HEADERS_CHECK]), + [ConfigKeys.REQUEST_METHOD_CHECK]: null, + ...tlsFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts new file mode 100644 index 00000000000000..10c52c295c9c49 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HTTPFields, ConfigKeys, ContentType, contentTypesToMode } from '../types'; +import { + Normalizer, + commonNormalizers, + getNormalizer, + getJsonToArrayOrObjectNormalizer, +} from '../common/normalizers'; +import { tlsNormalizers } from '../tls/normalizers'; +import { defaultHTTPSimpleFields, defaultHTTPAdvancedFields } from '../contexts'; + +export type HTTPNormalizerMap = Record; + +const defaultHTTPValues = { + ...defaultHTTPSimpleFields, + ...defaultHTTPAdvancedFields, +}; + +export const getHTTPNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, defaultHTTPValues); +}; + +export const getHTTPJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { + return getJsonToArrayOrObjectNormalizer(key, defaultHTTPValues); +}; + +export const httpNormalizers: HTTPNormalizerMap = { + [ConfigKeys.URLS]: getHTTPNormalizer(ConfigKeys.URLS), + [ConfigKeys.MAX_REDIRECTS]: getHTTPNormalizer(ConfigKeys.MAX_REDIRECTS), + [ConfigKeys.USERNAME]: getHTTPNormalizer(ConfigKeys.USERNAME), + [ConfigKeys.PASSWORD]: getHTTPNormalizer(ConfigKeys.PASSWORD), + [ConfigKeys.PROXY_URL]: getHTTPNormalizer(ConfigKeys.PROXY_URL), + [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE + ), + [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE + ), + [ConfigKeys.RESPONSE_BODY_INDEX]: getHTTPNormalizer(ConfigKeys.RESPONSE_BODY_INDEX), + [ConfigKeys.RESPONSE_HEADERS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.RESPONSE_HEADERS_CHECK + ), + [ConfigKeys.RESPONSE_HEADERS_INDEX]: getHTTPNormalizer(ConfigKeys.RESPONSE_HEADERS_INDEX), + [ConfigKeys.RESPONSE_STATUS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.RESPONSE_STATUS_CHECK + ), + [ConfigKeys.REQUEST_BODY_CHECK]: (fields) => { + const requestBody = fields?.[ConfigKeys.REQUEST_BODY_CHECK]?.value; + const requestHeaders = fields?.[ConfigKeys.REQUEST_HEADERS_CHECK]?.value; + if (requestBody) { + const headers = requestHeaders + ? JSON.parse(fields?.[ConfigKeys.REQUEST_HEADERS_CHECK]?.value) + : defaultHTTPAdvancedFields[ConfigKeys.REQUEST_HEADERS_CHECK]; + const requestBodyValue = + requestBody !== null && requestBody !== undefined + ? JSON.parse(requestBody) + : defaultHTTPAdvancedFields[ConfigKeys.REQUEST_BODY_CHECK]?.value; + let requestBodyType = defaultHTTPAdvancedFields[ConfigKeys.REQUEST_BODY_CHECK]?.type; + Object.keys(headers || []).some((headerKey) => { + if (headerKey === 'Content-Type' && contentTypesToMode[headers[headerKey] as ContentType]) { + requestBodyType = contentTypesToMode[headers[headerKey] as ContentType]; + return true; + } + }); + return { + value: requestBodyValue, + type: requestBodyType, + }; + } else { + return defaultHTTPAdvancedFields[ConfigKeys.REQUEST_BODY_CHECK]; + } + }, + [ConfigKeys.REQUEST_HEADERS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + ConfigKeys.REQUEST_HEADERS_CHECK + ), + [ConfigKeys.REQUEST_METHOD_CHECK]: getHTTPNormalizer(ConfigKeys.REQUEST_METHOD_CHECK), + ...commonNormalizers, + ...tlsNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx index d17b8c997e9e8d..8eb81eb92f7b42 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx @@ -33,7 +33,7 @@ export const HTTPSimpleFields = memo(({ validate }) => { defaultMessage="URL" /> } - isInvalid={!!validate[ConfigKeys.URLS]?.(fields[ConfigKeys.URLS])} + isInvalid={!!validate[ConfigKeys.URLS]?.(fields)} error={ (({ validate }) => { defaultMessage="Monitor interval" /> } - isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields[ConfigKeys.SCHEDULE])} + isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields)} error={ (({ validate }) => { defaultMessage="Max redirects" /> } - isInvalid={!!validate[ConfigKeys.MAX_REDIRECTS]?.(fields[ConfigKeys.MAX_REDIRECTS])} + isInvalid={!!validate[ConfigKeys.MAX_REDIRECTS]?.(fields)} error={ (({ validate }) => { defaultMessage="Timeout in seconds" /> } - isInvalid={ - !!validate[ConfigKeys.TIMEOUT]?.( - fields[ConfigKeys.TIMEOUT], - fields[ConfigKeys.SCHEDULE].number, - fields[ConfigKeys.SCHEDULE].unit - ) - } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} error={ - + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) } helpText={ ; + +export const icmpFormatters: ICMPFormatMap = { + [ConfigKeys.HOSTS]: null, + [ConfigKeys.WAIT]: (fields) => secondsToCronFormatter(fields[ConfigKeys.WAIT]), + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/icmp/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/icmp/normalizers.ts new file mode 100644 index 00000000000000..18ce1da00e117e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/icmp/normalizers.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ICMPFields, ConfigKeys } from '../types'; +import { + Normalizer, + commonNormalizers, + getNormalizer, + getCronNormalizer, +} from '../common/normalizers'; +import { defaultICMPSimpleFields } from '../contexts'; + +export type ICMPNormalizerMap = Record; + +export const getICMPNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, defaultICMPSimpleFields); +}; + +export const getICMPCronToSecondsNormalizer = (key: ConfigKeys) => { + return getCronNormalizer(key, defaultICMPSimpleFields); +}; + +export const icmpNormalizers: ICMPNormalizerMap = { + [ConfigKeys.HOSTS]: getICMPNormalizer(ConfigKeys.HOSTS), + [ConfigKeys.WAIT]: getICMPCronToSecondsNormalizer(ConfigKeys.WAIT), + ...commonNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx index 3ca07c70673677..420f218429e402 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx @@ -33,7 +33,7 @@ export const ICMPSimpleFields = memo(({ validate }) => { defaultMessage="Host" /> } - isInvalid={!!validate[ConfigKeys.HOSTS]?.(fields[ConfigKeys.HOSTS])} + isInvalid={!!validate[ConfigKeys.HOSTS]?.(fields)} error={ (({ validate }) => { defaultMessage="Monitor interval" /> } - isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields[ConfigKeys.SCHEDULE])} + isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields)} error={ (({ validate }) => { defaultMessage="Wait in seconds" /> } - isInvalid={!!validate[ConfigKeys.WAIT]?.(fields[ConfigKeys.WAIT])} + isInvalid={!!validate[ConfigKeys.WAIT]?.(fields)} error={ (({ validate }) => { defaultMessage="Timeout in seconds" /> } - isInvalid={ - !!validate[ConfigKeys.TIMEOUT]?.( - fields[ConfigKeys.TIMEOUT], - fields[ConfigKeys.SCHEDULE].number, - fields[ConfigKeys.SCHEDULE].unit - ) - } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} error={ - + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) } helpText={ ( ({ newPolicy, onChange }) => { - const { monitorType } = useContext(MonitorTypeContext); - const { fields: httpSimpleFields } = useContext(HTTPSimpleFieldsContext); - const { fields: tcpSimpleFields } = useContext(TCPSimpleFieldsContext); - const { fields: icmpSimpleFields } = useContext(ICMPSimpleFieldsContext); - const { fields: httpAdvancedFields } = useContext(HTTPAdvancedFieldsContext); - const { fields: tcpAdvancedFields } = useContext(TCPAdvancedFieldsContext); - const { fields: tlsFields } = useContext(TLSFieldsContext); + const { monitorType } = useMonitorTypeContext(); + const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext(); + const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext(); + const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext(); + const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext(); + const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext(); + const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext(); + const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext(); + const { fields: tlsFields } = useTLSFieldsContext(); + + const policyConfig: PolicyConfig = { + [DataStream.HTTP]: { + ...httpSimpleFields, + ...httpAdvancedFields, + ...tlsFields, + [ConfigKeys.NAME]: newPolicy.name, + } as HTTPFields, + [DataStream.TCP]: { + ...tcpSimpleFields, + ...tcpAdvancedFields, + ...tlsFields, + [ConfigKeys.NAME]: newPolicy.name, + } as TCPFields, + [DataStream.ICMP]: { + ...icmpSimpleFields, + [ConfigKeys.NAME]: newPolicy.name, + } as ICMPFields, + [DataStream.BROWSER]: { + ...browserSimpleFields, + ...browserAdvancedFields, + [ConfigKeys.NAME]: newPolicy.name, + } as BrowserFields, + }; + useTrackPageview({ app: 'fleet', path: 'syntheticsCreate' }); useTrackPageview({ app: 'fleet', path: 'syntheticsCreate', delay: 15000 }); - const { setConfig } = useUpdatePolicy({ + + const dataStreams: DataStream[] = useMemo(() => { + return newPolicy.inputs.map((input) => { + return input.type.replace(/synthetics\//g, '') as DataStream; + }); + }, [newPolicy]); + + useUpdatePolicy({ monitorType, - defaultConfig, + defaultConfig: defaultConfig[monitorType], + config: policyConfig[monitorType], newPolicy, onChange, validate, @@ -80,42 +130,7 @@ export const SyntheticsPolicyCreateExtension = memo { - setConfig(() => { - switch (monitorType) { - case DataStream.HTTP: - return { - ...httpSimpleFields, - ...httpAdvancedFields, - ...tlsFields, - }; - case DataStream.TCP: - return { - ...tcpSimpleFields, - ...tcpAdvancedFields, - ...tlsFields, - }; - case DataStream.ICMP: - return { - ...icmpSimpleFields, - }; - } - }); - }, - 250, - [ - setConfig, - httpSimpleFields, - tcpSimpleFields, - icmpSimpleFields, - httpAdvancedFields, - tcpAdvancedFields, - tlsFields, - ] - ); - - return ; + return ; } ); SyntheticsPolicyCreateExtension.displayName = 'SyntheticsPolicyCreateExtension'; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx index 395b5d67abeb0b..e642ea44ab58db 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import 'jest-canvas-mock'; + import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; import { render } from '../../lib/helper/rtl_helpers'; @@ -18,6 +20,10 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); +jest.mock('./code_editor', () => ({ + CodeEditor: () =>
code editor mock
, +})); + const defaultNewPolicy: NewPackagePolicy = { name: 'samplePolicyName', description: '', @@ -148,6 +154,7 @@ const defaultNewPolicy: NewPackagePolicy = { type: 'text', }, name: { + value: 'Sample name', type: 'text', }, schedule: { @@ -217,6 +224,7 @@ const defaultNewPolicy: NewPackagePolicy = { type: 'text', }, name: { + value: 'Sample name', type: 'text', }, schedule: { @@ -236,8 +244,53 @@ const defaultNewPolicy: NewPackagePolicy = { timeout: { type: 'text', }, - max_redirects: { - type: 'integer', + tags: { + type: 'yaml', + }, + }, + }, + ], + }, + { + type: 'synthetics/browser', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { + type: 'synthetics', + dataset: 'browser', + }, + vars: { + type: { + value: 'browser', + type: 'text', + }, + name: { + value: 'Sample name', + type: 'text', + }, + schedule: { + value: '10s', + type: 'text', + }, + 'source.zip_url.url': { + type: 'text', + }, + 'source.zip_url.username': { + type: 'text', + }, + 'source.zip_url.password': { + type: 'password', + }, + 'source.zip_url.folder': { + type: 'text', + }, + 'source.inline.script': { + type: 'yaml', + }, + timeout: { + type: 'text', }, tags: { type: 'yaml', @@ -263,6 +316,10 @@ describe('', () => { return ; }; + beforeEach(() => { + onChange.mockClear(); + }); + it('renders SyntheticsPolicyCreateExtension', async () => { const { getByText, getByLabelText, queryByLabelText } = render(); const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement; @@ -371,6 +428,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -406,6 +464,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -434,6 +493,7 @@ describe('', () => { enabled: true, }, defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -481,7 +541,7 @@ describe('', () => { const urlError = getByText('URL is required'); const monitorIntervalError = getByText('Monitor interval is required'); const maxRedirectsError = getByText('Max redirects must be 0 or greater'); - const timeoutError = getByText('Timeout must be 0 or greater and less than schedule interval'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(urlError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -508,9 +568,7 @@ describe('', () => { expect(queryByText('URL is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ isValid: true, @@ -537,9 +595,7 @@ describe('', () => { await waitFor(() => { const hostError = getByText('Host and port are required'); const monitorIntervalError = getByText('Monitor interval is required'); - const timeoutError = getByText( - 'Timeout must be 0 or greater and less than schedule interval' - ); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(hostError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -560,9 +616,7 @@ describe('', () => { expect(queryByText('Host and port are required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ isValid: true, @@ -591,9 +645,7 @@ describe('', () => { await waitFor(() => { const hostError = getByText('Host is required'); const monitorIntervalError = getByText('Monitor interval is required'); - const timeoutError = getByText( - 'Timeout must be 0 or greater and less than schedule interval' - ); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); const waitError = getByText('Wait must be 0 or greater'); expect(hostError).toBeInTheDocument(); @@ -616,9 +668,7 @@ describe('', () => { await waitFor(() => { expect(queryByText('Host is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(queryByText('Wait must be 0 or greater')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ @@ -628,13 +678,67 @@ describe('', () => { }); }); + it('handles browser validation', async () => { + const { getByText, getByLabelText, queryByText, getByRole } = render(); + + const monitorType = getByLabelText('Monitor Type') as HTMLInputElement; + fireEvent.change(monitorType, { target: { value: DataStream.BROWSER } }); + + const zipUrl = getByRole('textbox', { name: 'Zip URL' }) as HTMLInputElement; + const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement; + const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement; + + // create errors + fireEvent.change(zipUrl, { target: { value: '' } }); + fireEvent.change(monitorIntervalNumber, { target: { value: '-1' } }); + fireEvent.change(timeout, { target: { value: '-1' } }); + + await waitFor(() => { + const hostError = getByText('Zip URL is required'); + const monitorIntervalError = getByText('Monitor interval is required'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); + + expect(hostError).toBeInTheDocument(); + expect(monitorIntervalError).toBeInTheDocument(); + expect(timeoutError).toBeInTheDocument(); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: false, + }) + ); + }); + + // resolve errors + fireEvent.change(zipUrl, { target: { value: 'http://github.com/tests.zip' } }); + fireEvent.change(monitorIntervalNumber, { target: { value: '1' } }); + fireEvent.change(timeout, { target: { value: '1' } }); + + await waitFor(() => { + expect(queryByText('Zip URL is required')).not.toBeInTheDocument(); + expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: true, + }) + ); + }); + + // test inline script validation + fireEvent.click(getByText('Inline script')); + + await waitFor(() => { + expect(getByText('Script is required')).toBeInTheDocument(); + }); + }); + it('handles changing TLS fields', async () => { const { findByLabelText, queryByLabelText } = render(); const enableSSL = queryByLabelText('Enable TLS configuration') as HTMLInputElement; await waitFor(() => { expect(onChange).toBeCalledWith({ - isValid: true, + isValid: false, updatedPolicy: { ...defaultNewPolicy, inputs: [ @@ -671,6 +775,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -714,7 +819,7 @@ describe('', () => { await waitFor(() => { expect(onChange).toBeCalledWith({ - isValid: true, + isValid: false, updatedPolicy: { ...defaultNewPolicy, inputs: [ @@ -751,6 +856,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx index 88bb8e7871459d..0bc8f31f3d6cf0 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.tsx @@ -13,6 +13,7 @@ import { TCPContextProvider, ICMPSimpleFieldsContextProvider, HTTPContextProvider, + BrowserContextProvider, TLSFieldsContextProvider, } from './contexts'; @@ -28,7 +29,9 @@ export const SyntheticsPolicyCreateExtensionWrapper = memo - + + + diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx index 8a3c42c10bc14a..ec135e4e914a7c 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx @@ -6,7 +6,6 @@ */ import React, { memo } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; import { PackagePolicyEditExtensionComponentProps } from '../../../../fleet/public'; import { useTrackPageview } from '../../../../observability/public'; import { @@ -17,8 +16,19 @@ import { useHTTPSimpleFieldsContext, useHTTPAdvancedFieldsContext, useTLSFieldsContext, + useBrowserSimpleFieldsContext, + useBrowserAdvancedFieldsContext, } from './contexts'; -import { PolicyConfig, DataStream } from './types'; +import { + ICustomFields, + DataStream, + HTTPFields, + TCPFields, + ICMPFields, + BrowserFields, + ConfigKeys, + PolicyConfig, +} from './types'; import { CustomFields } from './custom_fields'; import { useUpdatePolicy } from './use_update_policy'; import { validate } from './validation'; @@ -26,7 +36,7 @@ import { validate } from './validation'; interface SyntheticsPolicyEditExtensionProps { newPolicy: PackagePolicyEditExtensionComponentProps['newPolicy']; onChange: PackagePolicyEditExtensionComponentProps['onChange']; - defaultConfig: PolicyConfig; + defaultConfig: Partial; isTLSEnabled: boolean; } /** @@ -44,49 +54,42 @@ export const SyntheticsPolicyEditExtension = memo { - setConfig(() => { - switch (monitorType) { - case DataStream.HTTP: - return { - ...httpSimpleFields, - ...httpAdvancedFields, - ...tlsFields, - }; - case DataStream.TCP: - return { - ...tcpSimpleFields, - ...tcpAdvancedFields, - ...tlsFields, - }; - case DataStream.ICMP: - return { - ...icmpSimpleFields, - }; - } - }); - }, - 250, - [ - setConfig, - httpSimpleFields, - httpAdvancedFields, - tcpSimpleFields, - tcpAdvancedFields, - icmpSimpleFields, - tlsFields, - ] - ); - return ; } ); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx index fec6c504a445f0..d3c9030e855971 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import 'jest-canvas-mock'; + import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; import { render } from '../../lib/helper/rtl_helpers'; @@ -18,6 +20,10 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); +jest.mock('./code_editor', () => ({ + CodeEditor: () =>
code editor mock
, +})); + const defaultNewPolicy: NewPackagePolicy = { name: 'samplePolicyName', description: '', @@ -247,6 +253,54 @@ const defaultNewPolicy: NewPackagePolicy = { }, ], }, + { + type: 'synthetics/browser', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { + type: 'synthetics', + dataset: 'browser', + }, + vars: { + type: { + value: 'browser', + type: 'text', + }, + name: { + value: 'Sample name', + type: 'text', + }, + schedule: { + value: '"@every 5s"', + type: 'text', + }, + 'source.zip_url.url': { + type: 'text', + }, + 'source.zip_url.username': { + type: 'text', + }, + 'source.zip_url.password': { + type: 'password', + }, + 'source.zip_url.folder': { + type: 'text', + }, + 'source.inline.script': { + type: 'yaml', + }, + timeout: { + type: 'text', + }, + tags: { + type: 'yaml', + }, + }, + }, + ], + }, ], package: { name: 'synthetics', @@ -268,6 +322,7 @@ const defaultCurrentPolicy: any = { const defaultHTTPConfig = defaultConfig[DataStream.HTTP]; const defaultICMPConfig = defaultConfig[DataStream.ICMP]; const defaultTCPConfig = defaultConfig[DataStream.TCP]; +const defaultBrowserConfig = defaultConfig[DataStream.BROWSER]; describe('', () => { const onChange = jest.fn(); @@ -281,6 +336,10 @@ describe('', () => { ); }; + beforeEach(() => { + onChange.mockClear(); + }); + it('renders SyntheticsPolicyEditExtension', async () => { const { getByText, getByLabelText, queryByLabelText } = render(); const url = getByLabelText('URL') as HTMLInputElement; @@ -400,6 +459,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -435,6 +495,7 @@ describe('', () => { }, defaultNewPolicy.inputs[1], defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }, }); @@ -458,7 +519,7 @@ describe('', () => { const urlError = getByText('URL is required'); const monitorIntervalError = getByText('Monitor interval is required'); const maxRedirectsError = getByText('Max redirects must be 0 or greater'); - const timeoutError = getByText('Timeout must be 0 or greater and less than schedule interval'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(urlError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -485,9 +546,7 @@ describe('', () => { expect(queryByText('URL is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); expect(queryByText('Max redirects must be 0 or greater')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ isValid: true, @@ -496,6 +555,82 @@ describe('', () => { }); }); + it('handles browser validation', async () => { + const currentPolicy = { + ...defaultCurrentPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[1], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[2], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[3], + enabled: true, + }, + ], + }; + const { getByText, getByLabelText, queryByText, getByRole } = render( + + ); + + const zipUrl = getByRole('textbox', { name: 'Zip URL' }) as HTMLInputElement; + const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement; + const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement; + + // create errors + fireEvent.change(zipUrl, { target: { value: '' } }); + fireEvent.change(monitorIntervalNumber, { target: { value: '-1' } }); + fireEvent.change(timeout, { target: { value: '-1' } }); + + await waitFor(() => { + const hostError = getByText('Zip URL is required'); + const monitorIntervalError = getByText('Monitor interval is required'); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); + + expect(hostError).toBeInTheDocument(); + expect(monitorIntervalError).toBeInTheDocument(); + expect(timeoutError).toBeInTheDocument(); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: false, + }) + ); + }); + + await waitFor(() => { + fireEvent.change(zipUrl, { target: { value: 'http://github.com/tests.zip' } }); + fireEvent.change(monitorIntervalNumber, { target: { value: '2' } }); + fireEvent.change(timeout, { target: { value: '1' } }); + expect(zipUrl.value).toEqual('http://github.com/tests.zip'); + expect(monitorIntervalNumber.value).toEqual('2'); + expect(timeout.value).toEqual('1'); + expect(queryByText('Zip URL is required')).not.toBeInTheDocument(); + expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: true, + }) + ); + }); + + await waitFor(() => { + expect(onChange).toBeCalledWith( + expect.objectContaining({ + isValid: true, + }) + ); + }); + }, 10000); + it('handles tcp validation', async () => { const currentPolicy = { ...defaultCurrentPolicy, @@ -509,6 +644,7 @@ describe('', () => { enabled: true, }, defaultNewPolicy.inputs[2], + defaultNewPolicy.inputs[3], ], }; const { getByText, getByLabelText, queryByText } = render( @@ -527,9 +663,7 @@ describe('', () => { await waitFor(() => { const hostError = getByText('Host and port are required'); const monitorIntervalError = getByText('Monitor interval is required'); - const timeoutError = getByText( - 'Timeout must be 0 or greater and less than schedule interval' - ); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); expect(hostError).toBeInTheDocument(); expect(monitorIntervalError).toBeInTheDocument(); @@ -549,9 +683,7 @@ describe('', () => { await waitFor(() => { expect(queryByText('Host is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ isValid: true, @@ -576,6 +708,7 @@ describe('', () => { ...defaultNewPolicy.inputs[2], enabled: true, }, + defaultNewPolicy.inputs[3], ], }; const { getByText, getByLabelText, queryByText } = render( @@ -596,9 +729,7 @@ describe('', () => { await waitFor(() => { const hostError = getByText('Host is required'); const monitorIntervalError = getByText('Monitor interval is required'); - const timeoutError = getByText( - 'Timeout must be 0 or greater and less than schedule interval' - ); + const timeoutError = getByText('Timeout must be greater than or equal to 0'); const waitError = getByText('Wait must be 0 or greater'); expect(hostError).toBeInTheDocument(); @@ -621,9 +752,7 @@ describe('', () => { await waitFor(() => { expect(queryByText('Host is required')).not.toBeInTheDocument(); expect(queryByText('Monitor interval is required')).not.toBeInTheDocument(); - expect( - queryByText('Timeout must be 0 or greater and less than schedule interval') - ).not.toBeInTheDocument(); + expect(queryByText('Timeout must be greater than or equal to 0')).not.toBeInTheDocument(); expect(queryByText('Wait must be 0 or greater')).not.toBeInTheDocument(); expect(onChange).toBeCalledWith( expect.objectContaining({ @@ -640,6 +769,7 @@ describe('', () => { inputs: [ { ...defaultNewPolicy.inputs[0], + enabled: true, streams: [ { ...defaultNewPolicy.inputs[0].streams[0], @@ -663,6 +793,7 @@ describe('', () => { }, defaultCurrentPolicy.inputs[1], defaultCurrentPolicy.inputs[2], + defaultCurrentPolicy.inputs[3], ], }; const { getByText, getByLabelText, queryByLabelText, queryByText } = render( @@ -782,7 +913,7 @@ describe('', () => { }); it('handles null values for icmp', async () => { - const tcpVars = defaultNewPolicy.inputs[1].streams[0].vars; + const icmpVars = defaultNewPolicy.inputs[2].streams[0].vars; const currentPolicy: NewPackagePolicy = { ...defaultCurrentPolicy, inputs: [ @@ -801,12 +932,12 @@ describe('', () => { { ...defaultNewPolicy.inputs[2].streams[0], vars: { - ...Object.keys(tcpVars || []).reduce< + ...Object.keys(icmpVars || []).reduce< Record >((acc, key) => { acc[key] = { value: undefined, - type: `${tcpVars?.[key].type}`, + type: `${icmpVars?.[key].type}`, }; return acc; }, {}), @@ -846,4 +977,72 @@ describe('', () => { expect(queryByLabelText('Url')).not.toBeInTheDocument(); expect(queryByLabelText('Proxy URL')).not.toBeInTheDocument(); }); + + it('handles null values for browser', async () => { + const browserVars = defaultNewPolicy.inputs[3].streams[0].vars; + const currentPolicy: NewPackagePolicy = { + ...defaultCurrentPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[1], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[2], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[3], + enabled: true, + streams: [ + { + ...defaultNewPolicy.inputs[3].streams[0], + vars: { + ...Object.keys(browserVars || []).reduce< + Record + >((acc, key) => { + acc[key] = { + value: undefined, + type: `${browserVars?.[key].type}`, + }; + return acc; + }, {}), + [ConfigKeys.MONITOR_TYPE]: { + value: DataStream.BROWSER, + type: 'text', + }, + }, + }, + ], + }, + ], + }; + const { getByLabelText, queryByLabelText, getByRole } = render( + + ); + const zipUrl = getByRole('textbox', { name: 'Zip URL' }) as HTMLInputElement; + const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement; + const monitorIntervalUnit = getByLabelText('Unit') as HTMLInputElement; + const apmServiceName = getByLabelText('APM service name') as HTMLInputElement; + const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement; + expect(zipUrl).toBeInTheDocument(); + expect(zipUrl.value).toEqual(defaultBrowserConfig[ConfigKeys.SOURCE_ZIP_URL]); + expect(monitorIntervalNumber).toBeInTheDocument(); + expect(monitorIntervalNumber.value).toEqual(defaultBrowserConfig[ConfigKeys.SCHEDULE].number); + expect(monitorIntervalUnit).toBeInTheDocument(); + expect(monitorIntervalUnit.value).toEqual(defaultBrowserConfig[ConfigKeys.SCHEDULE].unit); + expect(apmServiceName).toBeInTheDocument(); + expect(apmServiceName.value).toEqual(defaultBrowserConfig[ConfigKeys.APM_SERVICE_NAME]); + expect(timeout).toBeInTheDocument(); + expect(timeout.value).toEqual(`${defaultBrowserConfig[ConfigKeys.TIMEOUT]}`); + + // ensure other monitor type options are not in the DOM + expect(queryByLabelText('Url')).not.toBeInTheDocument(); + expect(queryByLabelText('Proxy URL')).not.toBeInTheDocument(); + expect(queryByLabelText('Host')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.tsx index 0bafef61166d26..d83130b21a0f11 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.tsx @@ -7,27 +7,17 @@ import React, { memo, useMemo } from 'react'; import { PackagePolicyEditExtensionComponentProps } from '../../../../fleet/public'; -import { - PolicyConfig, - ConfigKeys, - ContentType, - DataStream, - ICustomFields, - contentTypesToMode, -} from './types'; +import { PolicyConfig, ConfigKeys, DataStream, ITLSFields, ICustomFields } from './types'; import { SyntheticsPolicyEditExtension } from './synthetics_policy_edit_extension'; import { MonitorTypeContextProvider, HTTPContextProvider, TCPContextProvider, - defaultTCPSimpleFields, - defaultHTTPSimpleFields, - defaultICMPSimpleFields, - defaultHTTPAdvancedFields, - defaultTCPAdvancedFields, - defaultTLSFields, ICMPSimpleFieldsContextProvider, + BrowserContextProvider, + TLSFieldsContextProvider, } from './contexts'; +import { normalizers } from './helpers/normalizers'; /** * Exports Synthetics-specific package policy instructions @@ -35,123 +25,64 @@ import { */ export const SyntheticsPolicyEditExtensionWrapper = memo( ({ policy: currentPolicy, newPolicy, onChange }) => { - const { enableTLS: isTLSEnabled, config: defaultConfig, monitorType } = useMemo(() => { - const fallbackConfig: PolicyConfig = { - [DataStream.HTTP]: { - ...defaultHTTPSimpleFields, - ...defaultHTTPAdvancedFields, - ...defaultTLSFields, - }, - [DataStream.TCP]: { - ...defaultTCPSimpleFields, - ...defaultTCPAdvancedFields, - ...defaultTLSFields, - }, - [DataStream.ICMP]: defaultICMPSimpleFields, - }; + const { + enableTLS: isTLSEnabled, + fullConfig: fullDefaultConfig, + monitorTypeConfig: defaultConfig, + monitorType, + tlsConfig: defaultTLSConfig, + } = useMemo(() => { let enableTLS = false; const getDefaultConfig = () => { + // find the enabled input to identify the current monitor type const currentInput = currentPolicy.inputs.find((input) => input.enabled === true); - const vars = currentInput?.streams[0]?.vars; + /* Inputs can have multiple data streams. This is true of the `synthetics/browser` input, which includes the browser.network and browser.screenshot + * data streams. The `browser.network` and `browser.screenshot` data streams are used to store metadata and mappings. + * However, the `browser` data stream is where the variables for the policy are stored. For this reason, we only want + * to grab the data stream that exists within our explicitly defined list, which is the browser data stream */ + const vars = currentInput?.streams.find((stream) => + Object.values(DataStream).includes(stream.data_stream.dataset as DataStream) + )?.vars; + const type: DataStream = vars?.[ConfigKeys.MONITOR_TYPE].value as DataStream; - const fallbackConfigForMonitorType = fallbackConfig[type] as Partial; - const configKeys: ConfigKeys[] = Object.values(ConfigKeys); - const formatttedDefaultConfigForMonitorType = configKeys.reduce( - (acc: Record, key: ConfigKeys) => { - const value = vars?.[key]?.value; - switch (key) { - case ConfigKeys.NAME: - acc[key] = currentPolicy.name; - break; - case ConfigKeys.SCHEDULE: - // split unit and number - if (value) { - const fullString = JSON.parse(value); - const fullSchedule = fullString.replace('@every ', ''); - const unit = fullSchedule.slice(-1); - const number = fullSchedule.slice(0, fullSchedule.length - 1); - acc[key] = { - unit, - number, - }; - } else { - acc[key] = fallbackConfigForMonitorType[key]; - } - break; - case ConfigKeys.TIMEOUT: - case ConfigKeys.WAIT: - acc[key] = value - ? value.slice(0, value.length - 1) - : fallbackConfigForMonitorType[key]; // remove unit - break; - case ConfigKeys.TAGS: - case ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE: - case ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE: - case ConfigKeys.RESPONSE_STATUS_CHECK: - case ConfigKeys.RESPONSE_HEADERS_CHECK: - case ConfigKeys.REQUEST_HEADERS_CHECK: - acc[key] = value ? JSON.parse(value) : fallbackConfigForMonitorType[key]; - break; - case ConfigKeys.REQUEST_BODY_CHECK: - const headers = value - ? JSON.parse(vars?.[ConfigKeys.REQUEST_HEADERS_CHECK].value) - : fallbackConfigForMonitorType[ConfigKeys.REQUEST_HEADERS_CHECK]; - const requestBodyValue = - value !== null && value !== undefined - ? JSON.parse(value) - : fallbackConfigForMonitorType[key]?.value; - let requestBodyType = fallbackConfigForMonitorType[key]?.type; - Object.keys(headers || []).some((headerKey) => { - if ( - headerKey === 'Content-Type' && - contentTypesToMode[headers[headerKey] as ContentType] - ) { - requestBodyType = contentTypesToMode[headers[headerKey] as ContentType]; - return true; - } - }); - acc[key] = { - value: requestBodyValue, - type: requestBodyType, - }; - break; - case ConfigKeys.TLS_KEY_PASSPHRASE: - case ConfigKeys.TLS_VERIFICATION_MODE: - acc[key] = { - value: value ?? fallbackConfigForMonitorType[key]?.value, - isEnabled: !!value, - }; - if (!!value) { - enableTLS = true; - } - break; - case ConfigKeys.TLS_CERTIFICATE: - case ConfigKeys.TLS_CERTIFICATE_AUTHORITIES: - case ConfigKeys.TLS_KEY: - case ConfigKeys.TLS_VERSION: - acc[key] = { - value: value ? JSON.parse(value) : fallbackConfigForMonitorType[key]?.value, - isEnabled: !!value, - }; - if (!!value) { - enableTLS = true; - } - break; - default: - acc[key] = value ?? fallbackConfigForMonitorType[key]; - } - return acc; + const configKeys: ConfigKeys[] = Object.values(ConfigKeys) || ([] as ConfigKeys[]); + const formattedDefaultConfigForMonitorType: ICustomFields = configKeys.reduce( + (acc: ICustomFields, key: ConfigKeys) => { + return { + ...acc, + [key]: normalizers[key]?.(vars), + }; }, - {} + {} as ICustomFields ); - const formattedDefaultConfig: PolicyConfig = { - ...fallbackConfig, - [type]: formatttedDefaultConfigForMonitorType, + const tlsConfig: ITLSFields = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES], + [ConfigKeys.TLS_CERTIFICATE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_CERTIFICATE], + [ConfigKeys.TLS_KEY]: formattedDefaultConfigForMonitorType[ConfigKeys.TLS_KEY], + [ConfigKeys.TLS_KEY_PASSPHRASE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_KEY_PASSPHRASE], + [ConfigKeys.TLS_VERIFICATION_MODE]: + formattedDefaultConfigForMonitorType[ConfigKeys.TLS_VERIFICATION_MODE], + [ConfigKeys.TLS_VERSION]: formattedDefaultConfigForMonitorType[ConfigKeys.TLS_VERSION], }; - return { config: formattedDefaultConfig, enableTLS, monitorType: type }; + enableTLS = Object.values(tlsConfig).some((value) => value?.isEnabled); + + const formattedDefaultConfig: Partial = { + [type]: formattedDefaultConfigForMonitorType, + }; + + return { + fullConfig: formattedDefaultConfig, + monitorTypeConfig: formattedDefaultConfigForMonitorType, + tlsConfig, + enableTLS, + monitorType: type, + }; }; return getDefaultConfig(); @@ -159,18 +90,22 @@ export const SyntheticsPolicyEditExtensionWrapper = memo - - - - - - - + + + + + + + + + + + ); } diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/tcp/formatters.ts new file mode 100644 index 00000000000000..2f4a43ee6becf6 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/formatters.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TCPFields, ConfigKeys } from '../types'; +import { Formatter, commonFormatters } from '../common/formatters'; +import { tlsFormatters } from '../tls/formatters'; + +export type TCPFormatMap = Record; + +export const tcpFormatters: TCPFormatMap = { + [ConfigKeys.HOSTS]: null, + [ConfigKeys.PROXY_URL]: null, + [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: null, + [ConfigKeys.RESPONSE_RECEIVE_CHECK]: null, + [ConfigKeys.REQUEST_SEND_CHECK]: null, + ...tlsFormatters, + ...commonFormatters, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/tcp/normalizers.ts new file mode 100644 index 00000000000000..d19aea55addf2f --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/normalizers.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TCPFields, ConfigKeys } from '../types'; +import { Normalizer, commonNormalizers, getNormalizer } from '../common/normalizers'; +import { tlsNormalizers } from '../tls/normalizers'; +import { defaultTCPSimpleFields, defaultTCPAdvancedFields } from '../contexts'; + +const defaultTCPFields = { + ...defaultTCPSimpleFields, + ...defaultTCPAdvancedFields, +}; + +export type TCPNormalizerMap = Record; + +export const getTCPNormalizer = (key: ConfigKeys) => { + return getNormalizer(key, defaultTCPFields); +}; + +export const tcpNormalizers: TCPNormalizerMap = { + [ConfigKeys.HOSTS]: getTCPNormalizer(ConfigKeys.HOSTS), + [ConfigKeys.PROXY_URL]: getTCPNormalizer(ConfigKeys.PROXY_URL), + [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: getTCPNormalizer(ConfigKeys.PROXY_USE_LOCAL_RESOLVER), + [ConfigKeys.RESPONSE_RECEIVE_CHECK]: getTCPNormalizer(ConfigKeys.RESPONSE_RECEIVE_CHECK), + [ConfigKeys.REQUEST_SEND_CHECK]: getTCPNormalizer(ConfigKeys.REQUEST_SEND_CHECK), + ...tlsNormalizers, + ...commonNormalizers, +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx index 82c77a63611f2d..8bc017a51cfa9b 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx @@ -33,7 +33,7 @@ export const TCPSimpleFields = memo(({ validate }) => { defaultMessage="Host:Port" /> } - isInvalid={!!validate[ConfigKeys.HOSTS]?.(fields[ConfigKeys.HOSTS])} + isInvalid={!!validate[ConfigKeys.HOSTS]?.(fields)} error={ (({ validate }) => { defaultMessage="Monitor interval" /> } - isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields[ConfigKeys.SCHEDULE])} + isInvalid={!!validate[ConfigKeys.SCHEDULE]?.(fields)} error={ (({ validate }) => { defaultMessage="Timeout in seconds" /> } - isInvalid={ - !!validate[ConfigKeys.TIMEOUT]?.( - fields[ConfigKeys.TIMEOUT], - fields[ConfigKeys.SCHEDULE].number, - fields[ConfigKeys.SCHEDULE].unit - ) - } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} error={ - + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) } helpText={ ; + +export const tlsFormatters: TLSFormatMap = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: (fields) => + tlsValueToYamlFormatter(fields[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]), + [ConfigKeys.TLS_CERTIFICATE]: (fields) => + tlsValueToYamlFormatter(fields[ConfigKeys.TLS_CERTIFICATE]), + [ConfigKeys.TLS_KEY]: (fields) => tlsValueToYamlFormatter(fields[ConfigKeys.TLS_KEY]), + [ConfigKeys.TLS_KEY_PASSPHRASE]: (fields) => + tlsValueToStringFormatter(fields[ConfigKeys.TLS_KEY_PASSPHRASE]), + [ConfigKeys.TLS_VERIFICATION_MODE]: (fields) => + tlsValueToStringFormatter(fields[ConfigKeys.TLS_VERIFICATION_MODE]), + [ConfigKeys.TLS_VERSION]: (fields) => tlsArrayToYamlFormatter(fields[ConfigKeys.TLS_VERSION]), +}; + +// only add tls settings if they are enabled by the user and isEnabled is true +export const tlsValueToYamlFormatter = (tlsValue: { value?: string; isEnabled?: boolean } = {}) => + tlsValue.isEnabled && tlsValue.value ? JSON.stringify(tlsValue.value) : null; + +export const tlsValueToStringFormatter = (tlsValue: { value?: string; isEnabled?: boolean } = {}) => + tlsValue.isEnabled && tlsValue.value ? tlsValue.value : null; + +export const tlsArrayToYamlFormatter = (tlsValue: { value?: string[]; isEnabled?: boolean } = {}) => + tlsValue.isEnabled && tlsValue.value?.length ? JSON.stringify(tlsValue.value) : null; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tls/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/tls/normalizers.ts new file mode 100644 index 00000000000000..2344e599d6c015 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/tls/normalizers.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ITLSFields, ConfigKeys } from '../types'; +import { Normalizer } from '../common/normalizers'; +import { defaultTLSFields } from '../contexts'; + +type TLSNormalizerMap = Record; + +export const tlsNormalizers: TLSNormalizerMap = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: (fields) => + tlsYamlToObjectNormalizer( + fields?.[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]?.value, + ConfigKeys.TLS_CERTIFICATE_AUTHORITIES + ), + [ConfigKeys.TLS_CERTIFICATE]: (fields) => + tlsYamlToObjectNormalizer( + fields?.[ConfigKeys.TLS_CERTIFICATE]?.value, + ConfigKeys.TLS_CERTIFICATE + ), + [ConfigKeys.TLS_KEY]: (fields) => + tlsYamlToObjectNormalizer(fields?.[ConfigKeys.TLS_KEY]?.value, ConfigKeys.TLS_KEY), + [ConfigKeys.TLS_KEY_PASSPHRASE]: (fields) => + tlsStringToObjectNormalizer( + fields?.[ConfigKeys.TLS_KEY_PASSPHRASE]?.value, + ConfigKeys.TLS_KEY_PASSPHRASE + ), + [ConfigKeys.TLS_VERIFICATION_MODE]: (fields) => + tlsStringToObjectNormalizer( + fields?.[ConfigKeys.TLS_VERIFICATION_MODE]?.value, + ConfigKeys.TLS_VERIFICATION_MODE + ), + [ConfigKeys.TLS_VERSION]: (fields) => + tlsYamlToObjectNormalizer(fields?.[ConfigKeys.TLS_VERSION]?.value, ConfigKeys.TLS_VERSION), +}; + +// only add tls settings if they are enabled by the user and isEnabled is true +export const tlsStringToObjectNormalizer = (value: string = '', key: keyof ITLSFields) => ({ + value: value ?? defaultTLSFields[key]?.value, + isEnabled: Boolean(value), +}); +export const tlsYamlToObjectNormalizer = (value: string = '', key: keyof ITLSFields) => ({ + value: value ? JSON.parse(value) : defaultTLSFields[key]?.value, + isEnabled: Boolean(value), +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx index 7a16d1352c40a1..89581bf9933390 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx @@ -9,6 +9,7 @@ export enum DataStream { HTTP = 'http', TCP = 'tcp', ICMP = 'icmp', + BROWSER = 'browser', } export enum HTTPMethod { @@ -65,6 +66,12 @@ export enum TLSVersion { ONE_THREE = 'TLSv1.3', } +export enum ScreenshotOption { + ON = 'on', + OFF = 'off', + ONLY_ON_FAILURE = 'only-on-failure', +} + // values must match keys in the integration package export enum ConfigKeys { APM_SERVICE_NAME = 'service.name', @@ -87,6 +94,14 @@ export enum ConfigKeys { REQUEST_METHOD_CHECK = 'check.request.method', REQUEST_SEND_CHECK = 'check.send', SCHEDULE = 'schedule', + SCREENSHOTS = 'screenshots', + SOURCE_INLINE = 'source.inline.script', + SOURCE_ZIP_URL = 'source.zip_url.url', + SOURCE_ZIP_USERNAME = 'source.zip_url.username', + SOURCE_ZIP_PASSWORD = 'source.zip_url.password', + SOURCE_ZIP_FOLDER = 'source.zip_url.folder', + SYNTHETICS_ARGS = 'synthetics_args', + PARAMS = 'params', TLS_CERTIFICATE_AUTHORITIES = 'ssl.certificate_authorities', TLS_CERTIFICATE = 'ssl.certificate', TLS_KEY = 'ssl.key', @@ -100,18 +115,6 @@ export enum ConfigKeys { WAIT = 'wait', } -export interface ISimpleFields { - [ConfigKeys.HOSTS]: string; - [ConfigKeys.MAX_REDIRECTS]: string; - [ConfigKeys.MONITOR_TYPE]: DataStream; - [ConfigKeys.SCHEDULE]: { number: string; unit: ScheduleUnit }; - [ConfigKeys.APM_SERVICE_NAME]: string; - [ConfigKeys.TIMEOUT]: string; - [ConfigKeys.URLS]: string; - [ConfigKeys.TAGS]: string[]; - [ConfigKeys.WAIT]: string; -} - export interface ICommonFields { [ConfigKeys.MONITOR_TYPE]: DataStream; [ConfigKeys.SCHEDULE]: { number: string; unit: ScheduleUnit }; @@ -183,13 +186,29 @@ export interface ITCPAdvancedFields { [ConfigKeys.REQUEST_SEND_CHECK]: string; } +export type IBrowserSimpleFields = { + [ConfigKeys.SOURCE_INLINE]: string; + [ConfigKeys.SOURCE_ZIP_URL]: string; + [ConfigKeys.SOURCE_ZIP_FOLDER]: string; + [ConfigKeys.SOURCE_ZIP_USERNAME]: string; + [ConfigKeys.SOURCE_ZIP_PASSWORD]: string; + [ConfigKeys.PARAMS]: string; +} & ICommonFields; + +export interface IBrowserAdvancedFields { + [ConfigKeys.SYNTHETICS_ARGS]: string[]; + [ConfigKeys.SCREENSHOTS]: string; +} + export type HTTPFields = IHTTPSimpleFields & IHTTPAdvancedFields & ITLSFields; export type TCPFields = ITCPSimpleFields & ITCPAdvancedFields & ITLSFields; export type ICMPFields = IICMPSimpleFields; +export type BrowserFields = IBrowserSimpleFields & IBrowserAdvancedFields; export type ICustomFields = HTTPFields & TCPFields & - ICMPFields & { + ICMPFields & + BrowserFields & { [ConfigKeys.NAME]: string; }; @@ -197,9 +216,12 @@ export interface PolicyConfig { [DataStream.HTTP]: HTTPFields; [DataStream.TCP]: TCPFields; [DataStream.ICMP]: ICMPFields; + [DataStream.BROWSER]: BrowserFields; } -export type Validation = Partial boolean>>; +export type Validator = (config: Partial) => boolean; + +export type Validation = Partial>; export const contentTypesToMode = { [ContentType.FORM]: Mode.FORM, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx index 5a62aec90032d9..d57a69860311c0 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx @@ -6,10 +6,21 @@ */ import { useUpdatePolicy } from './use_update_policy'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; import { NewPackagePolicy } from '../../../../fleet/public'; import { validate } from './validation'; -import { ConfigKeys, DataStream, TLSVersion } from './types'; +import { + ConfigKeys, + DataStream, + TLSVersion, + ICommonFields, + ScheduleUnit, + ICMPFields, + TCPFields, + ITLSFields, + HTTPFields, + BrowserFields, +} from './types'; import { defaultConfig } from './synthetics_policy_create_extension'; describe('useBarChartsHooks', () => { @@ -245,6 +256,63 @@ describe('useBarChartsHooks', () => { }, ], }, + { + type: 'synthetics/browser', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { + type: 'synthetics', + dataset: 'browser', + }, + vars: { + type: { + value: 'browser', + type: 'text', + }, + name: { + value: 'Sample name', + type: 'text', + }, + schedule: { + value: '10s', + type: 'text', + }, + 'source.zip_url.url': { + type: 'text', + }, + 'source.zip_url.username': { + type: 'text', + }, + 'source.zip_url.password': { + type: 'password', + }, + 'source.zip_url.folder': { + type: 'text', + }, + 'source.inline.script': { + type: 'yaml', + }, + 'service.name': { + type: 'text', + }, + screenshots: { + type: 'text', + }, + synthetics_args: { + type: 'yaml', + }, + timeout: { + type: 'text', + }, + tags: { + type: 'yaml', + }, + }, + }, + ], + }, ], package: { name: 'synthetics', @@ -253,84 +321,117 @@ describe('useBarChartsHooks', () => { }, }; - it('handles http data stream', () => { + const defaultCommonFields: Partial = { + [ConfigKeys.APM_SERVICE_NAME]: 'APM Service name', + [ConfigKeys.TAGS]: ['some', 'tags'], + [ConfigKeys.SCHEDULE]: { + number: '5', + unit: ScheduleUnit.MINUTES, + }, + [ConfigKeys.TIMEOUT]: '17', + }; + + const defaultTLSFields: Partial = { + [ConfigKeys.TLS_CERTIFICATE_AUTHORITIES]: { + isEnabled: true, + value: 'ca', + }, + [ConfigKeys.TLS_CERTIFICATE]: { + isEnabled: true, + value: 'cert', + }, + [ConfigKeys.TLS_KEY]: { + isEnabled: true, + value: 'key', + }, + [ConfigKeys.TLS_KEY_PASSPHRASE]: { + isEnabled: true, + value: 'password', + }, + }; + + it('handles http data stream', async () => { const onChange = jest.fn(); - const { result } = renderHook((props) => useUpdatePolicy(props), { - initialProps: { defaultConfig, newPolicy, onChange, validate, monitorType: DataStream.HTTP }, + const initialProps = { + defaultConfig: defaultConfig[DataStream.HTTP], + config: defaultConfig[DataStream.HTTP], + newPolicy, + onChange, + validate, + monitorType: DataStream.HTTP, + }; + const { result, rerender, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, }); expect(result.current.config).toMatchObject({ ...defaultConfig[DataStream.HTTP] }); + const config: HTTPFields = { + ...defaultConfig[DataStream.HTTP], + ...defaultCommonFields, + ...defaultTLSFields, + [ConfigKeys.URLS]: 'url', + [ConfigKeys.PROXY_URL]: 'proxyUrl', + }; + // expect only http to be enabled expect(result.current.updatedPolicy.inputs[0].enabled).toBe(true); expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(false); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.MONITOR_TYPE].value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.MONITOR_TYPE]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.URLS].value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.URLS]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.SCHEDULE].value - ).toEqual( - JSON.stringify( - `@every ${defaultConfig[DataStream.HTTP][ConfigKeys.SCHEDULE].number}${ - defaultConfig[DataStream.HTTP][ConfigKeys.SCHEDULE].unit - }` - ) - ); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.PROXY_URL].value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.PROXY_URL]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.APM_SERVICE_NAME].value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.APM_SERVICE_NAME]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TIMEOUT].value - ).toEqual(`${defaultConfig[DataStream.HTTP][ConfigKeys.TIMEOUT]}s`); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE - ].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE - ].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_STATUS_CHECK] - .value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.REQUEST_HEADERS_CHECK] - .value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_HEADERS_CHECK] - .value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_BODY_INDEX] - .value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.RESPONSE_BODY_INDEX]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_HEADERS_INDEX] - .value - ).toEqual(defaultConfig[DataStream.HTTP][ConfigKeys.RESPONSE_HEADERS_INDEX]); + rerender({ + ...initialProps, + config, + }); + + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[0]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.MONITOR_TYPE].value).toEqual(config[ConfigKeys.MONITOR_TYPE]); + expect(vars?.[ConfigKeys.URLS].value).toEqual(config[ConfigKeys.URLS]); + expect(vars?.[ConfigKeys.SCHEDULE].value).toEqual( + JSON.stringify( + `@every ${config[ConfigKeys.SCHEDULE].number}${config[ConfigKeys.SCHEDULE].unit}` + ) + ); + expect(vars?.[ConfigKeys.PROXY_URL].value).toEqual(config[ConfigKeys.PROXY_URL]); + expect(vars?.[ConfigKeys.APM_SERVICE_NAME].value).toEqual( + config[ConfigKeys.APM_SERVICE_NAME] + ); + expect(vars?.[ConfigKeys.TIMEOUT].value).toEqual(`${config[ConfigKeys.TIMEOUT]}s`); + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_STATUS_CHECK].value).toEqual(null); + expect(vars?.[ConfigKeys.REQUEST_HEADERS_CHECK].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_HEADERS_CHECK].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_BODY_INDEX].value).toEqual( + config[ConfigKeys.RESPONSE_BODY_INDEX] + ); + expect(vars?.[ConfigKeys.RESPONSE_HEADERS_INDEX].value).toEqual( + config[ConfigKeys.RESPONSE_HEADERS_INDEX] + ); + }); }); - it('stringifies array values and returns null for empty array values', () => { + it('stringifies array values and returns null for empty array values', async () => { const onChange = jest.fn(); - const { result } = renderHook((props) => useUpdatePolicy(props), { - initialProps: { defaultConfig, newPolicy, onChange, validate, monitorType: DataStream.HTTP }, + const initialProps = { + defaultConfig: defaultConfig[DataStream.HTTP], + config: defaultConfig[DataStream.HTTP], + newPolicy, + onChange, + validate, + monitorType: DataStream.HTTP, + }; + const { rerender, result, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, }); - act(() => { - result.current.setConfig({ - ...defaultConfig, + rerender({ + ...initialProps, + config: { + ...defaultConfig[DataStream.HTTP], [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: ['test'], [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: ['test'], [ConfigKeys.RESPONSE_STATUS_CHECK]: ['test'], @@ -339,38 +440,29 @@ describe('useBarChartsHooks', () => { value: [TLSVersion.ONE_ONE], isEnabled: true, }, - }); + }, }); - // expect only http to be enabled - expect(result.current.updatedPolicy.inputs[0].enabled).toBe(true); - expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); - expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + await waitFor(() => { + // expect only http to be enabled + expect(result.current.updatedPolicy.inputs[0].enabled).toBe(true); + expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(false); + + const vars = result.current.updatedPolicy.inputs[0]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE].value).toEqual('["test"]'); + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE].value).toEqual('["test"]'); + expect(vars?.[ConfigKeys.RESPONSE_STATUS_CHECK].value).toEqual('["test"]'); + expect(vars?.[ConfigKeys.TAGS].value).toEqual('["test"]'); + expect(vars?.[ConfigKeys.TLS_VERSION].value).toEqual('["TLSv1.1"]'); + }); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE - ].value - ).toEqual('["test"]'); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE - ].value - ).toEqual('["test"]'); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_STATUS_CHECK] - .value - ).toEqual('["test"]'); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TAGS].value - ).toEqual('["test"]'); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TLS_VERSION].value - ).toEqual('["TLSv1.1"]'); - - act(() => { - result.current.setConfig({ - ...defaultConfig, + rerender({ + ...initialProps, + config: { + ...defaultConfig[DataStream.HTTP], [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: [], [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: [], [ConfigKeys.RESPONSE_STATUS_CHECK]: [], @@ -379,125 +471,207 @@ describe('useBarChartsHooks', () => { value: [], isEnabled: true, }, - }); + }, }); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE - ].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ - ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE - ].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_STATUS_CHECK] - .value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TAGS].value - ).toEqual(null); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.TLS_VERSION].value - ).toEqual(null); + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[0]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE].value).toEqual(null); + expect(vars?.[ConfigKeys.RESPONSE_STATUS_CHECK].value).toEqual(null); + expect(vars?.[ConfigKeys.TAGS].value).toEqual(null); + expect(vars?.[ConfigKeys.TLS_VERSION].value).toEqual(null); + }); }); - it('handles tcp data stream', () => { + it('handles tcp data stream', async () => { const onChange = jest.fn(); - const { result } = renderHook((props) => useUpdatePolicy(props), { - initialProps: { defaultConfig, newPolicy, onChange, validate, monitorType: DataStream.TCP }, + const initialProps = { + defaultConfig: defaultConfig[DataStream.TCP], + config: defaultConfig[DataStream.TCP], + newPolicy, + onChange, + validate, + monitorType: DataStream.TCP, + }; + const { result, rerender, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, }); // expect only tcp to be enabled expect(result.current.updatedPolicy.inputs[0].enabled).toBe(false); expect(result.current.updatedPolicy.inputs[1].enabled).toBe(true); expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(false); + + const config: TCPFields = { + ...defaultConfig[DataStream.TCP], + ...defaultCommonFields, + ...defaultTLSFields, + [ConfigKeys.HOSTS]: 'sampleHost', + [ConfigKeys.PROXY_URL]: 'proxyUrl', + [ConfigKeys.PROXY_USE_LOCAL_RESOLVER]: true, + [ConfigKeys.RESPONSE_RECEIVE_CHECK]: 'response', + [ConfigKeys.REQUEST_SEND_CHECK]: 'request', + }; - expect(onChange).toBeCalledWith({ - isValid: false, - updatedPolicy: result.current.updatedPolicy, + rerender({ + ...initialProps, + config, }); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.MONITOR_TYPE].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.MONITOR_TYPE]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.HOSTS].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.HOSTS]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.SCHEDULE].value - ).toEqual( - JSON.stringify( - `@every ${defaultConfig[DataStream.TCP][ConfigKeys.SCHEDULE].number}${ - defaultConfig[DataStream.TCP][ConfigKeys.SCHEDULE].unit - }` - ) - ); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.PROXY_URL].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.PROXY_URL]); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.APM_SERVICE_NAME].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.APM_SERVICE_NAME]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.TIMEOUT].value - ).toEqual(`${defaultConfig[DataStream.TCP][ConfigKeys.TIMEOUT]}s`); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ - ConfigKeys.PROXY_USE_LOCAL_RESOLVER - ].value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.PROXY_USE_LOCAL_RESOLVER]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.RESPONSE_RECEIVE_CHECK] - .value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.RESPONSE_RECEIVE_CHECK]); - expect( - result.current.updatedPolicy.inputs[1]?.streams[0]?.vars?.[ConfigKeys.REQUEST_SEND_CHECK] - .value - ).toEqual(defaultConfig[DataStream.TCP][ConfigKeys.REQUEST_SEND_CHECK]); + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[1]?.streams[0]?.vars; + + expect(onChange).toBeCalledWith({ + isValid: false, + updatedPolicy: result.current.updatedPolicy, + }); + + expect(vars?.[ConfigKeys.MONITOR_TYPE].value).toEqual(config[ConfigKeys.MONITOR_TYPE]); + expect(vars?.[ConfigKeys.HOSTS].value).toEqual(config[ConfigKeys.HOSTS]); + expect(vars?.[ConfigKeys.SCHEDULE].value).toEqual( + JSON.stringify( + `@every ${config[ConfigKeys.SCHEDULE].number}${config[ConfigKeys.SCHEDULE].unit}` + ) + ); + expect(vars?.[ConfigKeys.PROXY_URL].value).toEqual(config[ConfigKeys.PROXY_URL]); + expect(vars?.[ConfigKeys.APM_SERVICE_NAME].value).toEqual( + config[ConfigKeys.APM_SERVICE_NAME] + ); + expect(vars?.[ConfigKeys.TIMEOUT].value).toEqual(`${config[ConfigKeys.TIMEOUT]}s`); + expect(vars?.[ConfigKeys.PROXY_USE_LOCAL_RESOLVER].value).toEqual( + config[ConfigKeys.PROXY_USE_LOCAL_RESOLVER] + ); + expect(vars?.[ConfigKeys.RESPONSE_RECEIVE_CHECK].value).toEqual( + config[ConfigKeys.RESPONSE_RECEIVE_CHECK] + ); + expect(vars?.[ConfigKeys.REQUEST_SEND_CHECK].value).toEqual( + config[ConfigKeys.REQUEST_SEND_CHECK] + ); + }); }); - it('handles icmp data stream', () => { + it('handles icmp data stream', async () => { const onChange = jest.fn(); - const { result } = renderHook((props) => useUpdatePolicy(props), { - initialProps: { defaultConfig, newPolicy, onChange, validate, monitorType: DataStream.ICMP }, + const initialProps = { + defaultConfig: defaultConfig[DataStream.ICMP], + config: defaultConfig[DataStream.ICMP], + newPolicy, + onChange, + validate, + monitorType: DataStream.ICMP, + }; + const { rerender, result, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, }); + const config: ICMPFields = { + ...defaultConfig[DataStream.ICMP], + ...defaultCommonFields, + [ConfigKeys.WAIT]: '2', + [ConfigKeys.HOSTS]: 'sampleHost', + }; // expect only icmp to be enabled expect(result.current.updatedPolicy.inputs[0].enabled).toBe(false); expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); expect(result.current.updatedPolicy.inputs[2].enabled).toBe(true); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(false); + + // only call onChange when the policy is changed + rerender({ + ...initialProps, + config, + }); + + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[2]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.MONITOR_TYPE].value).toEqual(config[ConfigKeys.MONITOR_TYPE]); + expect(vars?.[ConfigKeys.HOSTS].value).toEqual(config[ConfigKeys.HOSTS]); + expect(vars?.[ConfigKeys.SCHEDULE].value).toEqual( + JSON.stringify( + `@every ${config[ConfigKeys.SCHEDULE].number}${config[ConfigKeys.SCHEDULE].unit}` + ) + ); + expect(vars?.[ConfigKeys.APM_SERVICE_NAME].value).toEqual( + config[ConfigKeys.APM_SERVICE_NAME] + ); + expect(vars?.[ConfigKeys.TIMEOUT].value).toEqual(`${config[ConfigKeys.TIMEOUT]}s`); + expect(vars?.[ConfigKeys.WAIT].value).toEqual(`${config[ConfigKeys.WAIT]}s`); + + expect(onChange).toBeCalledWith({ + isValid: false, + updatedPolicy: result.current.updatedPolicy, + }); + }); + }); - expect(onChange).toBeCalledWith({ - isValid: false, - updatedPolicy: result.current.updatedPolicy, + it('handles browser data stream', async () => { + const onChange = jest.fn(); + const initialProps = { + defaultConfig: defaultConfig[DataStream.BROWSER], + config: defaultConfig[DataStream.BROWSER], + newPolicy, + onChange, + validate, + monitorType: DataStream.BROWSER, + }; + const { result, rerender, waitFor } = renderHook((props) => useUpdatePolicy(props), { + initialProps, + }); + + // expect only browser to be enabled + expect(result.current.updatedPolicy.inputs[0].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[1].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[2].enabled).toBe(false); + expect(result.current.updatedPolicy.inputs[3].enabled).toBe(true); + + const config: BrowserFields = { + ...defaultConfig[DataStream.BROWSER], + ...defaultCommonFields, + [ConfigKeys.SOURCE_INLINE]: 'inlineScript', + [ConfigKeys.SOURCE_ZIP_URL]: 'zipFolder', + [ConfigKeys.SOURCE_ZIP_FOLDER]: 'zipFolder', + [ConfigKeys.SOURCE_ZIP_USERNAME]: 'username', + [ConfigKeys.SOURCE_ZIP_PASSWORD]: 'password', + [ConfigKeys.SCREENSHOTS]: 'off', + [ConfigKeys.SYNTHETICS_ARGS]: ['args'], + }; + + rerender({ + ...initialProps, + config, }); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.MONITOR_TYPE].value - ).toEqual(defaultConfig[DataStream.ICMP][ConfigKeys.MONITOR_TYPE]); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.HOSTS].value - ).toEqual(defaultConfig[DataStream.ICMP][ConfigKeys.HOSTS]); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.SCHEDULE].value - ).toEqual( - JSON.stringify( - `@every ${defaultConfig[DataStream.ICMP][ConfigKeys.SCHEDULE].number}${ - defaultConfig[DataStream.ICMP][ConfigKeys.SCHEDULE].unit - }` - ) - ); - expect( - result.current.updatedPolicy.inputs[0]?.streams[0]?.vars?.[ConfigKeys.APM_SERVICE_NAME].value - ).toEqual(defaultConfig[DataStream.ICMP][ConfigKeys.APM_SERVICE_NAME]); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.TIMEOUT].value - ).toEqual(`${defaultConfig[DataStream.ICMP][ConfigKeys.TIMEOUT]}s`); - expect( - result.current.updatedPolicy.inputs[2]?.streams[0]?.vars?.[ConfigKeys.WAIT].value - ).toEqual(`${defaultConfig[DataStream.ICMP][ConfigKeys.WAIT]}s`); + await waitFor(() => { + const vars = result.current.updatedPolicy.inputs[3]?.streams[0]?.vars; + + expect(vars?.[ConfigKeys.SOURCE_ZIP_FOLDER].value).toEqual( + config[ConfigKeys.SOURCE_ZIP_FOLDER] + ); + expect(vars?.[ConfigKeys.SOURCE_ZIP_PASSWORD].value).toEqual( + config[ConfigKeys.SOURCE_ZIP_PASSWORD] + ); + expect(vars?.[ConfigKeys.SOURCE_ZIP_URL].value).toEqual(config[ConfigKeys.SOURCE_ZIP_URL]); + expect(vars?.[ConfigKeys.SOURCE_INLINE].value).toEqual(config[ConfigKeys.SOURCE_INLINE]); + expect(vars?.[ConfigKeys.SOURCE_ZIP_PASSWORD].value).toEqual( + config[ConfigKeys.SOURCE_ZIP_PASSWORD] + ); + expect(vars?.[ConfigKeys.SCREENSHOTS].value).toEqual(config[ConfigKeys.SCREENSHOTS]); + expect(vars?.[ConfigKeys.SYNTHETICS_ARGS].value).toEqual( + JSON.stringify(config[ConfigKeys.SYNTHETICS_ARGS]) + ); + expect(vars?.[ConfigKeys.APM_SERVICE_NAME].value).toEqual( + config[ConfigKeys.APM_SERVICE_NAME] + ); + expect(vars?.[ConfigKeys.TIMEOUT].value).toEqual(`${config[ConfigKeys.TIMEOUT]}s`); + + expect(onChange).toBeCalledWith({ + isValid: false, + updatedPolicy: result.current.updatedPolicy, + }); + }); }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts index 2b2fb22866463f..145a86c6bd50d8 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts @@ -6,11 +6,13 @@ */ import { useEffect, useRef, useState } from 'react'; import { NewPackagePolicy } from '../../../../fleet/public'; -import { ConfigKeys, PolicyConfig, DataStream, Validation, ICustomFields } from './types'; +import { ConfigKeys, DataStream, Validation, ICustomFields } from './types'; +import { formatters } from './helpers/formatters'; interface Props { monitorType: DataStream; - defaultConfig: PolicyConfig; + defaultConfig: Partial; + config: Partial; newPolicy: NewPackagePolicy; onChange: (opts: { /** is current form state is valid */ @@ -24,85 +26,45 @@ interface Props { export const useUpdatePolicy = ({ monitorType, defaultConfig, + config, newPolicy, onChange, validate, }: Props) => { const [updatedPolicy, setUpdatedPolicy] = useState(newPolicy); // Update the integration policy with our custom fields - const [config, setConfig] = useState>(defaultConfig[monitorType]); - const currentConfig = useRef>(defaultConfig[monitorType]); + const currentConfig = useRef>(defaultConfig); useEffect(() => { const configKeys = Object.keys(config) as ConfigKeys[]; const validationKeys = Object.keys(validate[monitorType]) as ConfigKeys[]; const configDidUpdate = configKeys.some((key) => config[key] !== currentConfig.current[key]); const isValid = - !!newPolicy.name && !validationKeys.find((key) => validate[monitorType][key]?.(config[key])); + !!newPolicy.name && !validationKeys.find((key) => validate[monitorType]?.[key]?.(config)); const formattedPolicy = { ...newPolicy }; const currentInput = formattedPolicy.inputs.find( (input) => input.type === `synthetics/${monitorType}` ); - const dataStream = currentInput?.streams[0]; - - // prevent an infinite loop of updating the policy - if (currentInput && dataStream && configDidUpdate) { + const dataStream = currentInput?.streams.find( + (stream) => stream.data_stream.dataset === monitorType + ); + formattedPolicy.inputs.forEach((input) => (input.enabled = false)); + if (currentInput && dataStream) { // reset all data streams to enabled false formattedPolicy.inputs.forEach((input) => (input.enabled = false)); // enable only the input type and data stream that matches the monitor type. currentInput.enabled = true; dataStream.enabled = true; + } + + // prevent an infinite loop of updating the policy + if (currentInput && dataStream && configDidUpdate) { configKeys.forEach((key) => { const configItem = dataStream.vars?.[key]; - if (configItem) { - switch (key) { - case ConfigKeys.SCHEDULE: - configItem.value = JSON.stringify( - `@every ${config[key]?.number}${config[key]?.unit}` - ); // convert to cron - break; - case ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE: - case ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE: - case ConfigKeys.RESPONSE_STATUS_CHECK: - case ConfigKeys.TAGS: - configItem.value = config[key]?.length ? JSON.stringify(config[key]) : null; - break; - case ConfigKeys.RESPONSE_HEADERS_CHECK: - case ConfigKeys.REQUEST_HEADERS_CHECK: - configItem.value = Object.keys(config?.[key] || []).length - ? JSON.stringify(config[key]) - : null; - break; - case ConfigKeys.TIMEOUT: - case ConfigKeys.WAIT: - configItem.value = config[key] ? `${config[key]}s` : null; // convert to cron - break; - case ConfigKeys.REQUEST_BODY_CHECK: - configItem.value = config[key]?.value ? JSON.stringify(config[key]?.value) : null; // only need value of REQUEST_BODY_CHECK for outputted policy - break; - case ConfigKeys.TLS_CERTIFICATE: - case ConfigKeys.TLS_CERTIFICATE_AUTHORITIES: - case ConfigKeys.TLS_KEY: - configItem.value = - config[key]?.isEnabled && config[key]?.value - ? JSON.stringify(config[key]?.value) - : null; // only add tls settings if they are enabled by the user - break; - case ConfigKeys.TLS_VERSION: - configItem.value = - config[key]?.isEnabled && config[key]?.value.length - ? JSON.stringify(config[key]?.value) - : null; // only add tls settings if they are enabled by the user - break; - case ConfigKeys.TLS_KEY_PASSPHRASE: - case ConfigKeys.TLS_VERIFICATION_MODE: - configItem.value = - config[key]?.isEnabled && config[key]?.value ? config[key]?.value : null; // only add tls settings if they are enabled by the user - break; - default: - configItem.value = - config[key] === undefined || config[key] === null ? null : config[key]; - } + if (configItem && formatters[key]) { + configItem.value = formatters[key]?.(config); + } else if (configItem) { + configItem.value = config[key] === undefined || config[key] === null ? null : config[key]; } }); currentConfig.current = config; @@ -114,14 +76,8 @@ export const useUpdatePolicy = ({ } }, [config, currentConfig, newPolicy, onChange, validate, monitorType]); - // update our local config state ever time name, which is managed by fleet, changes - useEffect(() => { - setConfig((prevConfig) => ({ ...prevConfig, name: newPolicy.name })); - }, [newPolicy.name, setConfig]); - return { config, - setConfig, updatedPolicy, }; }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/validation.tsx b/x-pack/plugins/uptime/public/components/fleet_package/validation.tsx index f3057baf10381f..0ce5dc6f9f02d9 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/validation.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/validation.tsx @@ -4,11 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ConfigKeys, DataStream, ICustomFields, Validation, ScheduleUnit } from './types'; +import { + ConfigKeys, + DataStream, + ICustomFields, + Validator, + Validation, + ScheduleUnit, +} from './types'; export const digitsOnly = /^[0-9]*$/g; export const includesValidPort = /[^\:]+:[0-9]{1,5}$/g; +type ValidationLibrary = Record; + // returns true if invalid function validateHeaders(headers: T): boolean { return Object.keys(headers).some((key) => { @@ -22,7 +31,7 @@ function validateHeaders(headers: T): boolean { } // returns true if invalid -function validateTimeout({ +const validateTimeout = ({ scheduleNumber, scheduleUnit, timeout, @@ -30,7 +39,7 @@ function validateTimeout({ scheduleNumber: string; scheduleUnit: ScheduleUnit; timeout: string; -}): boolean { +}): boolean => { let schedule: number; switch (scheduleUnit) { case ScheduleUnit.SECONDS: @@ -44,69 +53,83 @@ function validateTimeout({ } return parseFloat(timeout) > schedule; -} +}; // validation functions return true when invalid -const validateCommon = { - [ConfigKeys.SCHEDULE]: (value: unknown) => { +const validateCommon: ValidationLibrary = { + [ConfigKeys.SCHEDULE]: ({ [ConfigKeys.SCHEDULE]: value }) => { const { number, unit } = value as ICustomFields[ConfigKeys.SCHEDULE]; const parsedFloat = parseFloat(number); return !parsedFloat || !unit || parsedFloat < 1; }, - [ConfigKeys.TIMEOUT]: ( - timeoutValue: unknown, - scheduleNumber: string, - scheduleUnit: ScheduleUnit - ) => - !timeoutValue || - parseFloat(timeoutValue as ICustomFields[ConfigKeys.TIMEOUT]) < 0 || - validateTimeout({ - timeout: timeoutValue as ICustomFields[ConfigKeys.TIMEOUT], - scheduleNumber, - scheduleUnit, - }), + [ConfigKeys.TIMEOUT]: ({ [ConfigKeys.TIMEOUT]: timeout, [ConfigKeys.SCHEDULE]: schedule }) => { + const { number, unit } = schedule as ICustomFields[ConfigKeys.SCHEDULE]; + + return ( + !timeout || + parseFloat(timeout) < 0 || + validateTimeout({ + timeout, + scheduleNumber: number, + scheduleUnit: unit, + }) + ); + }, }; -const validateHTTP = { - [ConfigKeys.RESPONSE_STATUS_CHECK]: (value: unknown) => { +const validateHTTP: ValidationLibrary = { + [ConfigKeys.RESPONSE_STATUS_CHECK]: ({ [ConfigKeys.RESPONSE_STATUS_CHECK]: value }) => { const statusCodes = value as ICustomFields[ConfigKeys.RESPONSE_STATUS_CHECK]; return statusCodes.length ? statusCodes.some((code) => !`${code}`.match(digitsOnly)) : false; }, - [ConfigKeys.RESPONSE_HEADERS_CHECK]: (value: unknown) => { + [ConfigKeys.RESPONSE_HEADERS_CHECK]: ({ [ConfigKeys.RESPONSE_HEADERS_CHECK]: value }) => { const headers = value as ICustomFields[ConfigKeys.RESPONSE_HEADERS_CHECK]; return validateHeaders(headers); }, - [ConfigKeys.REQUEST_HEADERS_CHECK]: (value: unknown) => { + [ConfigKeys.REQUEST_HEADERS_CHECK]: ({ [ConfigKeys.REQUEST_HEADERS_CHECK]: value }) => { const headers = value as ICustomFields[ConfigKeys.REQUEST_HEADERS_CHECK]; return validateHeaders(headers); }, - [ConfigKeys.MAX_REDIRECTS]: (value: unknown) => + [ConfigKeys.MAX_REDIRECTS]: ({ [ConfigKeys.MAX_REDIRECTS]: value }) => (!!value && !`${value}`.match(digitsOnly)) || parseFloat(value as ICustomFields[ConfigKeys.MAX_REDIRECTS]) < 0, - [ConfigKeys.URLS]: (value: unknown) => !value, + [ConfigKeys.URLS]: ({ [ConfigKeys.URLS]: value }) => !value, ...validateCommon, }; -const validateTCP = { - [ConfigKeys.HOSTS]: (value: unknown) => { +const validateTCP: Record = { + [ConfigKeys.HOSTS]: ({ [ConfigKeys.HOSTS]: value }) => { return !value || !`${value}`.match(includesValidPort); }, ...validateCommon, }; -const validateICMP = { - [ConfigKeys.HOSTS]: (value: unknown) => !value, - [ConfigKeys.WAIT]: (value: unknown) => +const validateICMP: ValidationLibrary = { + [ConfigKeys.HOSTS]: ({ [ConfigKeys.HOSTS]: value }) => !value, + [ConfigKeys.WAIT]: ({ [ConfigKeys.WAIT]: value }) => !!value && !digitsOnly.test(`${value}`) && parseFloat(value as ICustomFields[ConfigKeys.WAIT]) < 0, ...validateCommon, }; +const validateBrowser: ValidationLibrary = { + ...validateCommon, + [ConfigKeys.SOURCE_ZIP_URL]: ({ + [ConfigKeys.SOURCE_ZIP_URL]: zipUrl, + [ConfigKeys.SOURCE_INLINE]: inlineScript, + }) => !zipUrl && !inlineScript, + [ConfigKeys.SOURCE_INLINE]: ({ + [ConfigKeys.SOURCE_ZIP_URL]: zipUrl, + [ConfigKeys.SOURCE_INLINE]: inlineScript, + }) => !zipUrl && !inlineScript, +}; + export type ValidateDictionary = Record; export const validate: ValidateDictionary = { [DataStream.HTTP]: validateHTTP, [DataStream.TCP]: validateTCP, [DataStream.ICMP]: validateICMP, + [DataStream.BROWSER]: validateBrowser, }; From 0640a11feb1f92237c3ce4ba3f82b6fa924ed472 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 17 Aug 2021 16:54:36 +0300 Subject: [PATCH 09/11] Import EuiCodeEditor from kibana (#108846) --- src/plugins/advanced_settings/kibana.json | 2 +- .../public/management_app/components/field/field.tsx | 3 +-- src/plugins/advanced_settings/tsconfig.json | 1 + src/plugins/vis_type_vega/kibana.json | 2 +- .../vis_type_vega/public/components/vega_vis_editor.tsx | 2 +- src/plugins/vis_type_vega/tsconfig.json | 1 + 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index e524d78a53e80a..cf00241ee27664 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["management"], "optionalPlugins": ["home", "usageCollection"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "home"], + "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "esUiShared"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 759e1f992808fc..745452a31ff9c8 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -8,7 +8,6 @@ import React, { PureComponent, Fragment } from 'react'; import classNames from 'classnames'; - import 'brace/theme/textmate'; import 'brace/mode/markdown'; import 'brace/mode/json'; @@ -19,7 +18,6 @@ import { EuiCodeBlock, EuiColorPicker, EuiScreenReaderOnly, - EuiCodeEditor, EuiDescribedFormGroup, EuiFieldNumber, EuiFieldText, @@ -40,6 +38,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { FieldSetting, FieldState } from '../../types'; import { isDefaultValue } from '../../lib'; import { UiSettingsType, DocLinksStart, ToastsStart } from '../../../../../../core/public'; +import { EuiCodeEditor } from '../../../../../es_ui_shared/public'; interface FieldProps { setting: FieldSetting; diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index b207f600cbd4e9..5bf4ce3d6248bd 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -16,5 +16,6 @@ { "path": "../home/tsconfig.json" }, { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, ] } diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index 407e20fe0688a5..a579e85c0caf22 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["data", "visualizations", "mapsEms", "expressions", "inspector"], "optionalPlugins": ["home","usageCollection"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor", "esUiShared"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx index 148c630ad94e59..9150b31343799a 100644 --- a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx @@ -7,13 +7,13 @@ */ import React, { useCallback } from 'react'; -import { EuiCodeEditor } from '@elastic/eui'; import compactStringify from 'json-stringify-pretty-compact'; import hjson from 'hjson'; import 'brace/mode/hjson'; import { i18n } from '@kbn/i18n'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { EuiCodeEditor } from '../../../es_ui_shared/public'; import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index e1b8b5d9d4bac8..62bdd0262b4a53 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -26,5 +26,6 @@ { "path": "../kibana_utils/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, { "path": "../vis_default_editor/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, ] } From 9258ba5147026fb27786a5b9c1b60858659032bd Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Tue, 17 Aug 2021 10:21:06 -0400 Subject: [PATCH 10/11] Adding owners to kibana plugins (#108407) * Adding owners to kibana plugins * Fix ui actions enhanced owner * Account for virtual RAC team owning a plugin * Fix empty githubTeam for ui actions --- .github/CODEOWNERS | 5 ++++ examples/bfetch_explorer/kibana.json | 4 +++ examples/developer_examples/kibana.json | 4 +++ examples/expressions_explorer/kibana.json | 4 +++ src/plugins/console/kibana.json | 4 +++ src/plugins/dev_tools/kibana.json | 4 +++ src/plugins/embeddable/kibana.json | 19 +++++-------- src/plugins/es_ui_shared/kibana.json | 8 +++--- src/plugins/expressions/kibana.json | 9 ++++--- src/plugins/inspector/kibana.json | 4 +++ src/plugins/kibana_react/kibana.json | 4 +++ src/plugins/kibana_utils/kibana.json | 10 +++---- src/plugins/maps_ems/kibana.json | 4 +++ src/plugins/maps_legacy/kibana.json | 4 +++ src/plugins/navigation/kibana.json | 4 +++ src/plugins/region_map/kibana.json | 10 +++---- src/plugins/screenshot_mode/kibana.json | 4 +++ src/plugins/share/kibana.json | 4 +++ src/plugins/tile_map/kibana.json | 10 +++---- src/plugins/ui_actions/kibana.json | 9 ++++--- src/plugins/url_forwarding/kibana.json | 4 +++ x-pack/examples/alerting_example/kibana.json | 15 ++++++++++- .../ui_actions_enhanced_examples/kibana.json | 11 ++++---- x-pack/plugins/actions/kibana.json | 4 +++ x-pack/plugins/alerting/kibana.json | 4 +++ .../cross_cluster_replication/kibana.json | 14 +++++----- x-pack/plugins/data_enhanced/kibana.json | 4 +++ .../drilldowns/url_drilldown/kibana.json | 4 +++ .../plugins/embeddable_enhanced/kibana.json | 4 +++ x-pack/plugins/event_log/kibana.json | 4 +++ x-pack/plugins/grokdebugger/kibana.json | 15 +++++------ .../index_lifecycle_management/kibana.json | 25 +++++------------ x-pack/plugins/index_management/kibana.json | 10 +++---- x-pack/plugins/ingest_pipelines/kibana.json | 4 +++ x-pack/plugins/license_api_guard/kibana.json | 4 +++ x-pack/plugins/license_management/kibana.json | 10 +++---- x-pack/plugins/lists/kibana.json | 4 +++ x-pack/plugins/logstash/kibana.json | 16 +++++------ x-pack/plugins/maps/kibana.json | 27 ++++++------------- x-pack/plugins/metrics_entities/kibana.json | 4 +++ x-pack/plugins/monitoring/kibana.json | 4 +++ x-pack/plugins/osquery/kibana.json | 27 ++++++------------- x-pack/plugins/painless_lab/kibana.json | 19 +++++-------- x-pack/plugins/remote_clusters/kibana.json | 26 ++++++------------ x-pack/plugins/rollup/kibana.json | 25 +++++------------ x-pack/plugins/rule_registry/kibana.json | 11 ++------ x-pack/plugins/runtime_fields/kibana.json | 15 +++++------ x-pack/plugins/searchprofiler/kibana.json | 4 +++ x-pack/plugins/security_solution/kibana.json | 4 +++ x-pack/plugins/snapshot_restore/kibana.json | 23 +++++----------- x-pack/plugins/stack_alerts/kibana.json | 13 ++++++++- x-pack/plugins/task_manager/kibana.json | 4 +++ x-pack/plugins/timelines/kibana.json | 4 +++ .../plugins/triggers_actions_ui/kibana.json | 4 +++ .../plugins/ui_actions_enhanced/kibana.json | 16 +++++------ x-pack/plugins/upgrade_assistant/kibana.json | 4 +++ x-pack/plugins/watcher/kibana.json | 10 +++---- x-pack/plugins/xpack_legacy/kibana.json | 8 +++--- 58 files changed, 298 insertions(+), 238 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7ee84b6bc9e8d8..d116b1d3a41fc3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,6 +9,9 @@ /dev_docs @elastic/kibana-tech-leads /packages/kbn-docs-utils/ @elastic/kibana-tech-leads @elastic/kibana-operations +# Virtual teams +/x-pack/plugins/rule_registry/ @elastic/rac + # App /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app @@ -31,6 +34,7 @@ /src/plugins/vis_type_pie/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/visualizations/ @elastic/kibana-app +/src/plugins/url_forwarding/ @elastic/kibana-app /packages/kbn-tinymath/ @elastic/kibana-app # Application Services @@ -369,6 +373,7 @@ /x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/security-solution /x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution /x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/plugins/metrics_entities/ @elastic/security-solution /x-pack/test/detection_engine_api_integration @elastic/security-solution /x-pack/test/lists_api_integration @elastic/security-solution /x-pack/test/api_integration/apis/security_solution @elastic/security-solution diff --git a/examples/bfetch_explorer/kibana.json b/examples/bfetch_explorer/kibana.json index 4bd44926118126..0eda11670034c9 100644 --- a/examples/bfetch_explorer/kibana.json +++ b/examples/bfetch_explorer/kibana.json @@ -4,6 +4,10 @@ "version": "0.0.1", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["bfetch", "developerExamples"], "optionalPlugins": [], "requiredBundles": ["kibanaReact"] diff --git a/examples/developer_examples/kibana.json b/examples/developer_examples/kibana.json index 9e6b54c7af67c7..a744b53137dc76 100644 --- a/examples/developer_examples/kibana.json +++ b/examples/developer_examples/kibana.json @@ -1,5 +1,9 @@ { "id": "developerExamples", + "owner": { + "name": "Kibana Core", + "githubTeam": "kibana-core" + }, "kibanaVersion": "kibana", "version": "0.0.1", "ui": true diff --git a/examples/expressions_explorer/kibana.json b/examples/expressions_explorer/kibana.json index 7e2062ff0a5883..770ce91143d990 100644 --- a/examples/expressions_explorer/kibana.json +++ b/examples/expressions_explorer/kibana.json @@ -4,6 +4,10 @@ "version": "0.0.1", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["expressions", "inspector", "uiActions", "developerExamples"], "optionalPlugins": [], "requiredBundles": [] diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json index ca43e4f258adda..9452f43647a194 100644 --- a/src/plugins/console/kibana.json +++ b/src/plugins/console/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["devTools"], "optionalPlugins": ["usageCollection", "home"], "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"] diff --git a/src/plugins/dev_tools/kibana.json b/src/plugins/dev_tools/kibana.json index f1c6c9ecf87e61..75a1e82f1d910b 100644 --- a/src/plugins/dev_tools/kibana.json +++ b/src/plugins/dev_tools/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["urlForwarding"] } diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 1ecf76dbbd5c2a..42dc716fe64e97 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -3,16 +3,11 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "inspector", - "uiActions" - ], - "extraPublicDirs": [ - "public/lib/test_samples" - ], - "requiredBundles": [ - "savedObjects", - "kibanaReact", - "kibanaUtils" - ] + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredPlugins": ["inspector", "uiActions"], + "extraPublicDirs": ["public/lib/test_samples"], + "requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json index d442bfb93d5afa..2735b153f738c7 100644 --- a/src/plugins/es_ui_shared/kibana.json +++ b/src/plugins/es_ui_shared/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "ui": true, "server": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "extraPublicDirs": [ "static/validators/string", "static/forms/hook_form_lib", @@ -10,7 +14,5 @@ "static/forms/components", "static/forms/helpers/field_validators/types" ], - "requiredBundles": [ - "data" - ] + "requiredBundles": ["data"] } diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json index 23c7fe722fdb35..46e6ef8b4ea751 100644 --- a/src/plugins/expressions/kibana.json +++ b/src/plugins/expressions/kibana.json @@ -3,9 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common", "common/fonts"], - "requiredBundles": [ - "kibanaUtils", - "inspector" - ] + "requiredBundles": ["kibanaUtils", "inspector"] } diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json index 90e5d602507283..66c6617924a7e4 100644 --- a/src/plugins/inspector/kibana.json +++ b/src/plugins/inspector/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common", "common/adapters/request"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/kibana_react/kibana.json b/src/plugins/kibana_react/kibana.json index 6bf7ff1d82070d..210b15897cfad4 100644 --- a/src/plugins/kibana_react/kibana.json +++ b/src/plugins/kibana_react/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "ui": true, "server": false, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common"] } diff --git a/src/plugins/kibana_utils/kibana.json b/src/plugins/kibana_utils/kibana.json index 3e20b68bca431e..7c0f73a160970b 100644 --- a/src/plugins/kibana_utils/kibana.json +++ b/src/plugins/kibana_utils/kibana.json @@ -3,9 +3,9 @@ "version": "kibana", "ui": true, "server": false, - "extraPublicDirs": [ - "common", - "demos/state_containers/todomvc", - "common/state_containers" - ] + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "extraPublicDirs": ["common", "demos/state_containers/todomvc", "common/state_containers"] } diff --git a/src/plugins/maps_ems/kibana.json b/src/plugins/maps_ems/kibana.json index a7cf580becfd5e..0807867e9dcb39 100644 --- a/src/plugins/maps_ems/kibana.json +++ b/src/plugins/maps_ems/kibana.json @@ -1,5 +1,9 @@ { "id": "mapsEms", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["map"], diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index f321274791a3b4..fde5ad7b7adf5d 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -1,5 +1,9 @@ { "id": "mapsLegacy", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "ui": true, diff --git a/src/plugins/navigation/kibana.json b/src/plugins/navigation/kibana.json index 85d2049a34be0b..aa1294847cef84 100644 --- a/src/plugins/navigation/kibana.json +++ b/src/plugins/navigation/kibana.json @@ -1,5 +1,9 @@ { "id": "navigation", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "version": "kibana", "server": false, "ui": true, diff --git a/src/plugins/region_map/kibana.json b/src/plugins/region_map/kibana.json index 18ae8ec7eec8c6..1a24b6f8f05e8b 100644 --- a/src/plugins/region_map/kibana.json +++ b/src/plugins/region_map/kibana.json @@ -1,5 +1,9 @@ { "id": "regionMap", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "ui": true, @@ -13,9 +17,5 @@ "data", "share" ], - "requiredBundles": [ - "kibanaUtils", - "charts", - "visDefaultEditor" - ] + "requiredBundles": ["kibanaUtils", "charts", "visDefaultEditor"] } diff --git a/src/plugins/screenshot_mode/kibana.json b/src/plugins/screenshot_mode/kibana.json index 67c40b20be525a..98942569dfac8d 100644 --- a/src/plugins/screenshot_mode/kibana.json +++ b/src/plugins/screenshot_mode/kibana.json @@ -1,5 +1,9 @@ { "id": "screenshotMode", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "version": "1.0.0", "kibanaVersion": "kibana", "ui": true, diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index 8b1d28b1606d45..5580b723a095aa 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredBundles": ["kibanaUtils"], "optionalPlugins": ["securityOss"] } diff --git a/src/plugins/tile_map/kibana.json b/src/plugins/tile_map/kibana.json index 16be04b5189de8..48ed613b72cd34 100644 --- a/src/plugins/tile_map/kibana.json +++ b/src/plugins/tile_map/kibana.json @@ -1,5 +1,9 @@ { "id": "tileMap", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "ui": true, @@ -13,9 +17,5 @@ "data", "share" ], - "requiredBundles": [ - "kibanaUtils", - "charts", - "visDefaultEditor" - ] + "requiredBundles": ["kibanaUtils", "charts", "visDefaultEditor"] } diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json index ca979aa0210264..d112f6310a1fe9 100644 --- a/src/plugins/ui_actions/kibana.json +++ b/src/plugins/ui_actions/kibana.json @@ -3,8 +3,9 @@ "version": "kibana", "server": false, "ui": true, - "requiredBundles": [ - "kibanaUtils", - "kibanaReact" - ] + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/url_forwarding/kibana.json b/src/plugins/url_forwarding/kibana.json index 4f534c1219b344..253466631f2e2c 100644 --- a/src/plugins/url_forwarding/kibana.json +++ b/src/plugins/url_forwarding/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + }, "requiredPlugins": ["kibanaLegacy"] } diff --git a/x-pack/examples/alerting_example/kibana.json b/x-pack/examples/alerting_example/kibana.json index f2950db96ba2c7..13117713a9a7e9 100644 --- a/x-pack/examples/alerting_example/kibana.json +++ b/x-pack/examples/alerting_example/kibana.json @@ -2,9 +2,22 @@ "id": "alertingExample", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "server": true, "ui": true, - "requiredPlugins": ["triggersActionsUi", "charts", "data", "alerting", "actions", "kibanaReact", "features", "developerExamples"], + "requiredPlugins": [ + "triggersActionsUi", + "charts", + "data", + "alerting", + "actions", + "kibanaReact", + "features", + "developerExamples" + ], "optionalPlugins": [], "requiredBundles": ["kibanaReact"] } diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json index 59a0926118962e..4fa62668dd5573 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -5,6 +5,10 @@ "configPath": ["ui_actions_enhanced_examples"], "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": [ "uiActions", "uiActionsEnhanced", @@ -15,10 +19,5 @@ "developerExamples" ], "optionalPlugins": [], - "requiredBundles": [ - "dashboardEnhanced", - "embeddable", - "kibanaUtils", - "kibanaReact" - ] + "requiredBundles": ["dashboardEnhanced", "embeddable", "kibanaUtils", "kibanaReact"] } diff --git a/x-pack/plugins/actions/kibana.json b/x-pack/plugins/actions/kibana.json index ef604a9cf64173..aa3a9f3f6c34c2 100644 --- a/x-pack/plugins/actions/kibana.json +++ b/x-pack/plugins/actions/kibana.json @@ -1,5 +1,9 @@ { "id": "actions", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "server": true, "version": "8.0.0", "kibanaVersion": "kibana", diff --git a/x-pack/plugins/alerting/kibana.json b/x-pack/plugins/alerting/kibana.json index af2d08e69f597c..82d8de0daf14aa 100644 --- a/x-pack/plugins/alerting/kibana.json +++ b/x-pack/plugins/alerting/kibana.json @@ -2,6 +2,10 @@ "id": "alerting", "server": true, "ui": true, + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "alerting"], diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json index f130d0173cc89c..0a594cf1cc2ac7 100644 --- a/x-pack/plugins/cross_cluster_replication/kibana.json +++ b/x-pack/plugins/cross_cluster_replication/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": [ "home", "licensing", @@ -12,13 +16,7 @@ "indexManagement", "features" ], - "optionalPlugins": [ - "usageCollection" - ], + "optionalPlugins": ["usageCollection"], "configPath": ["xpack", "ccr"], - "requiredBundles": [ - "kibanaReact", - "esUiShared", - "data" - ] + "requiredBundles": ["kibanaReact", "esUiShared", "data"] } diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index da83ded471d0b3..d678921e9ac7bf 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -3,6 +3,10 @@ "id": "dataEnhanced", "version": "8.0.0", "kibanaVersion": "kibana", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "configPath": ["xpack", "data_enhanced"], "requiredPlugins": ["bfetch", "data", "features", "management", "share", "taskManager"], "optionalPlugins": ["kibanaUtils", "usageCollection", "security"], diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.json b/x-pack/plugins/drilldowns/url_drilldown/kibana.json index 9bdd13fbfea26d..a4552d201f2633 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/kibana.json +++ b/x-pack/plugins/drilldowns/url_drilldown/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["embeddable", "uiActions", "uiActionsEnhanced"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index 8d49e3e26eb7be..09416ce18aecb1 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/event_log/kibana.json b/x-pack/plugins/event_log/kibana.json index 0231bb6234471d..5223549a2e4fb1 100644 --- a/x-pack/plugins/event_log/kibana.json +++ b/x-pack/plugins/event_log/kibana.json @@ -2,6 +2,10 @@ "id": "eventLog", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "configPath": ["xpack", "eventLog"], "optionalPlugins": ["spaces"], "server": true, diff --git a/x-pack/plugins/grokdebugger/kibana.json b/x-pack/plugins/grokdebugger/kibana.json index 5f288e0cf3bdba..692aa16329d549 100644 --- a/x-pack/plugins/grokdebugger/kibana.json +++ b/x-pack/plugins/grokdebugger/kibana.json @@ -2,16 +2,13 @@ "id": "grokdebugger", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": [ - "licensing", - "home", - "devTools" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "home", "devTools"], "server": true, "ui": true, "configPath": ["xpack", "grokdebugger"], - "requiredBundles": [ - "kibanaReact", - "esUiShared" - ] + "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json index 21e7e7888acb92..bccb3cd78e78dc 100644 --- a/x-pack/plugins/index_lifecycle_management/kibana.json +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -3,23 +3,12 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "licensing", - "management", - "features", - "share" - ], - "optionalPlugins": [ - "cloud", - "usageCollection", - "indexManagement", - "home" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "management", "features", "share"], + "optionalPlugins": ["cloud", "usageCollection", "indexManagement", "home"], "configPath": ["xpack", "ilm"], - "requiredBundles": [ - "indexManagement", - "kibanaReact", - "esUiShared", - "home" - ] + "requiredBundles": ["indexManagement", "kibanaReact", "esUiShared", "home"] } diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index cd29e7b9ee1cdc..456ce830f6b57e 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -1,14 +1,14 @@ { "id": "indexManagement", + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "version": "kibana", "server": true, "ui": true, "requiredPlugins": ["home", "management", "features", "share"], "optionalPlugins": ["security", "usageCollection", "fleet"], "configPath": ["xpack", "index_management"], - "requiredBundles": [ - "kibanaReact", - "esUiShared", - "runtimeFields" - ] + "requiredBundles": ["kibanaReact", "esUiShared", "runtimeFields"] } diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 7c54d18fbd3826..800d92b5c97488 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -3,6 +3,10 @@ "version": "8.0.0", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["management", "features", "share"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], diff --git a/x-pack/plugins/license_api_guard/kibana.json b/x-pack/plugins/license_api_guard/kibana.json index 0fdf7ffed89880..1b870810ccbed4 100644 --- a/x-pack/plugins/license_api_guard/kibana.json +++ b/x-pack/plugins/license_api_guard/kibana.json @@ -2,6 +2,10 @@ "id": "licenseApiGuard", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "configPath": ["xpack", "licenseApiGuard"], "server": true, "ui": false diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json index be2e21c7eb41e0..a06bfbb9409fc4 100644 --- a/x-pack/plugins/license_management/kibana.json +++ b/x-pack/plugins/license_management/kibana.json @@ -3,13 +3,13 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["home", "licensing", "management", "features"], "optionalPlugins": ["telemetry"], "configPath": ["xpack", "license_management"], "extraPublicDirs": ["common/constants"], - "requiredBundles": [ - "telemetryManagementSection", - "esUiShared", - "kibanaReact" - ] + "requiredBundles": ["telemetryManagementSection", "esUiShared", "kibanaReact"] } diff --git a/x-pack/plugins/lists/kibana.json b/x-pack/plugins/lists/kibana.json index ae7b3e7679e0be..17a900b3f6fdc3 100644 --- a/x-pack/plugins/lists/kibana.json +++ b/x-pack/plugins/lists/kibana.json @@ -2,6 +2,10 @@ "configPath": ["xpack", "lists"], "extraPublicDirs": ["common"], "id": "lists", + "owner": { + "name": "Security detections response", + "githubTeam": "security-detections-response" + }, "kibanaVersion": "kibana", "requiredPlugins": [], "optionalPlugins": ["spaces", "security"], diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json index 0d14312a154e03..2ff4aac9ba55bb 100644 --- a/x-pack/plugins/logstash/kibana.json +++ b/x-pack/plugins/logstash/kibana.json @@ -1,18 +1,14 @@ { "id": "logstash", + "owner": { + "name": "Logstash", + "githubTeam": "logstash" + }, "version": "0.0.1", "kibanaVersion": "kibana", "configPath": ["xpack", "logstash"], - "requiredPlugins": [ - "licensing", - "management", - "features" - ], - "optionalPlugins": [ - "home", - "monitoring", - "security" - ], + "requiredPlugins": ["licensing", "management", "features"], + "optionalPlugins": ["home", "monitoring", "security"], "server": true, "ui": true, "requiredBundles": ["home"] diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 1cccfaa7748b1f..8ee4315aa62e6f 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -1,11 +1,12 @@ { "id": "maps", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "maps" - ], + "configPath": ["xpack", "maps"], "requiredPlugins": [ "licensing", "features", @@ -23,21 +24,9 @@ "share", "presentationUtil" ], - "optionalPlugins": [ - "home", - "savedObjectsTagging", - "charts", - "security" - ], + "optionalPlugins": ["home", "savedObjectsTagging", "charts", "security"], "ui": true, "server": true, - "extraPublicDirs": [ - "common/constants" - ], - "requiredBundles": [ - "kibanaReact", - "kibanaUtils", - "home", - "mapsEms" - ] + "extraPublicDirs": ["common/constants"], + "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "mapsEms"] } diff --git a/x-pack/plugins/metrics_entities/kibana.json b/x-pack/plugins/metrics_entities/kibana.json index 17484c2c243ce5..9d3a4f7f66a8d2 100644 --- a/x-pack/plugins/metrics_entities/kibana.json +++ b/x-pack/plugins/metrics_entities/kibana.json @@ -1,5 +1,9 @@ { "id": "metricsEntities", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "metricsEntities"], diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 7d82006c6b9998..0e02812af28fbb 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -2,6 +2,10 @@ "id": "monitoring", "version": "8.0.0", "kibanaVersion": "kibana", + "owner": { + "owner": "Stack Monitoring", + "githubTeam": "stack-monitoring-ui" + }, "configPath": ["monitoring"], "requiredPlugins": [ "licensing", diff --git a/x-pack/plugins/osquery/kibana.json b/x-pack/plugins/osquery/kibana.json index f947866641c281..a499b2b75ee685 100644 --- a/x-pack/plugins/osquery/kibana.json +++ b/x-pack/plugins/osquery/kibana.json @@ -1,25 +1,14 @@ { - "configPath": [ - "xpack", - "osquery" - ], - "extraPublicDirs": [ - "common" - ], + "configPath": ["xpack", "osquery"], + "extraPublicDirs": ["common"], "id": "osquery", + "owner": { + "name": "Security asset management", + "githubTeam": "security-asset-management" + }, "kibanaVersion": "kibana", - "optionalPlugins": [ - "fleet", - "home", - "usageCollection", - "lens" - ], - "requiredBundles": [ - "esUiShared", - "fleet", - "kibanaUtils", - "kibanaReact" - ], + "optionalPlugins": ["fleet", "home", "usageCollection", "lens"], + "requiredBundles": ["esUiShared", "fleet", "kibanaUtils", "kibanaReact"], "requiredPlugins": [ "actions", "data", diff --git a/x-pack/plugins/painless_lab/kibana.json b/x-pack/plugins/painless_lab/kibana.json index ca97e73704e705..7c71d4bdb4b761 100644 --- a/x-pack/plugins/painless_lab/kibana.json +++ b/x-pack/plugins/painless_lab/kibana.json @@ -2,18 +2,13 @@ "id": "painlessLab", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": [ - "devTools", - "licensing", - "home" - ], - "configPath": [ - "xpack", - "painless_lab" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["devTools", "licensing", "home"], + "configPath": ["xpack", "painless_lab"], "server": true, "ui": true, - "requiredBundles": [ - "kibanaReact" - ] + "requiredBundles": ["kibanaReact"] } diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json index 0334af5a868f23..192a1308c265a3 100644 --- a/x-pack/plugins/remote_clusters/kibana.json +++ b/x-pack/plugins/remote_clusters/kibana.json @@ -1,24 +1,14 @@ { "id": "remoteClusters", "version": "kibana", - "configPath": [ - "xpack", - "remote_clusters" - ], - "requiredPlugins": [ - "licensing", - "management", - "indexManagement", - "features" - ], - "optionalPlugins": [ - "usageCollection", - "cloud" - ], + "configPath": ["xpack", "remote_clusters"], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "management", "indexManagement", "features"], + "optionalPlugins": ["usageCollection", "cloud"], "server": true, "ui": true, - "requiredBundles": [ - "kibanaReact", - "esUiShared" - ] + "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index 10541d9a4ebddc..20f284686f3b58 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -4,23 +4,12 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "management", - "licensing", - "features" - ], - "optionalPlugins": [ - "home", - "indexManagement", - "usageCollection", - "visTypeTimeseries" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["management", "licensing", "features"], + "optionalPlugins": ["home", "indexManagement", "usageCollection", "visTypeTimeseries"], "configPath": ["xpack", "rollup"], - "requiredBundles": [ - "kibanaUtils", - "kibanaReact", - "home", - "esUiShared", - "data" - ] + "requiredBundles": ["kibanaUtils", "kibanaReact", "home", "esUiShared", "data"] } diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 360ea18df9ca14..a750c4a91072a3 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -2,15 +2,8 @@ "id": "ruleRegistry", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "ruleRegistry" - ], - "requiredPlugins": [ - "alerting", - "data", - "triggersActionsUi" - ], + "configPath": ["xpack", "ruleRegistry"], + "requiredPlugins": ["alerting", "data", "triggersActionsUi"], "optionalPlugins": ["security"], "server": true } diff --git a/x-pack/plugins/runtime_fields/kibana.json b/x-pack/plugins/runtime_fields/kibana.json index 65932c723c4742..ef5514a01b3cf4 100644 --- a/x-pack/plugins/runtime_fields/kibana.json +++ b/x-pack/plugins/runtime_fields/kibana.json @@ -3,13 +3,12 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": [ - ], - "optionalPlugins": [ - ], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredPlugins": [], + "optionalPlugins": [], "configPath": ["xpack", "runtime_fields"], - "requiredBundles": [ - "kibanaReact", - "esUiShared" - ] + "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/searchprofiler/kibana.json b/x-pack/plugins/searchprofiler/kibana.json index 6c94701c0ec09e..864e3880ae200e 100644 --- a/x-pack/plugins/searchprofiler/kibana.json +++ b/x-pack/plugins/searchprofiler/kibana.json @@ -5,6 +5,10 @@ "configPath": ["xpack", "searchprofiler"], "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["devTools", "home", "licensing"], "requiredBundles": ["esUiShared"] } diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 990756f3da7015..c8678a227510ea 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -1,5 +1,9 @@ { "id": "securitySolution", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "8.0.0", "extraPublicDirs": ["common"], "kibanaVersion": "kibana", diff --git a/x-pack/plugins/snapshot_restore/kibana.json b/x-pack/plugins/snapshot_restore/kibana.json index a8a3881929f400..bd2a85126c0c6e 100644 --- a/x-pack/plugins/snapshot_restore/kibana.json +++ b/x-pack/plugins/snapshot_restore/kibana.json @@ -3,21 +3,12 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "licensing", - "management", - "features" - ], - "optionalPlugins": [ - "usageCollection", - "security", - "cloud", - "home" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "management", "features"], + "optionalPlugins": ["usageCollection", "security", "cloud", "home"], "configPath": ["xpack", "snapshot_restore"], - "requiredBundles": [ - "esUiShared", - "kibanaReact", - "home" - ] + "requiredBundles": ["esUiShared", "kibanaReact", "home"] } diff --git a/x-pack/plugins/stack_alerts/kibana.json b/x-pack/plugins/stack_alerts/kibana.json index ed9f7d4e635f4e..1b4271328c2f95 100644 --- a/x-pack/plugins/stack_alerts/kibana.json +++ b/x-pack/plugins/stack_alerts/kibana.json @@ -1,9 +1,20 @@ { "id": "stackAlerts", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerting", "features", "triggersActionsUi", "kibanaReact", "savedObjects", "data"], + "requiredPlugins": [ + "alerting", + "features", + "triggersActionsUi", + "kibanaReact", + "savedObjects", + "data" + ], "configPath": ["xpack", "stack_alerts"], "requiredBundles": ["esUiShared"], "ui": true diff --git a/x-pack/plugins/task_manager/kibana.json b/x-pack/plugins/task_manager/kibana.json index aab1cd0ab41a5e..d0b847ce58d77b 100644 --- a/x-pack/plugins/task_manager/kibana.json +++ b/x-pack/plugins/task_manager/kibana.json @@ -2,6 +2,10 @@ "id": "taskManager", "server": true, "version": "8.0.0", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "kibanaVersion": "kibana", "configPath": ["xpack", "task_manager"], "optionalPlugins": ["usageCollection"], diff --git a/x-pack/plugins/timelines/kibana.json b/x-pack/plugins/timelines/kibana.json index bc9fba2c4a1bb7..0239dcdd8f1660 100644 --- a/x-pack/plugins/timelines/kibana.json +++ b/x-pack/plugins/timelines/kibana.json @@ -1,5 +1,9 @@ { "id": "timelines", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "timelines"], diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index a302673c2ec080..4033889d9811eb 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -1,5 +1,9 @@ { "id": "triggersActionsUi", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "version": "kibana", "server": true, "ui": true, diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json index dbc136a2588842..6fcac67c5a66b3 100644 --- a/x-pack/plugins/ui_actions_enhanced/kibana.json +++ b/x-pack/plugins/ui_actions_enhanced/kibana.json @@ -1,17 +1,13 @@ { "id": "uiActionsEnhanced", + "owner": { + "name": "Kibana App Services", + "githubTeam": "kibana-app-services" + }, "version": "kibana", "configPath": ["xpack", "ui_actions_enhanced"], "server": true, "ui": true, - "requiredPlugins": [ - "embeddable", - "uiActions", - "licensing" - ], - "requiredBundles": [ - "kibanaUtils", - "kibanaReact", - "data" - ] + "requiredPlugins": ["embeddable", "uiActions", "licensing"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "data"] } diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json index d013c16837b776..e69e352104f35d 100644 --- a/x-pack/plugins/upgrade_assistant/kibana.json +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "configPath": ["xpack", "upgrade_assistant"], "requiredPlugins": ["management", "licensing", "features"], "optionalPlugins": ["usageCollection"], diff --git a/x-pack/plugins/watcher/kibana.json b/x-pack/plugins/watcher/kibana.json index 84fe2b509b2637..6c9e46e0647afa 100644 --- a/x-pack/plugins/watcher/kibana.json +++ b/x-pack/plugins/watcher/kibana.json @@ -2,6 +2,10 @@ "id": "watcher", "configPath": ["xpack", "watcher"], "version": "kibana", + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": [ "home", "licensing", @@ -13,9 +17,5 @@ ], "server": true, "ui": true, - "requiredBundles": [ - "esUiShared", - "kibanaReact", - "fieldFormats" - ] + "requiredBundles": ["esUiShared", "kibanaReact", "fieldFormats"] } diff --git a/x-pack/plugins/xpack_legacy/kibana.json b/x-pack/plugins/xpack_legacy/kibana.json index fc45b612d72cf3..9dd0ac83401837 100644 --- a/x-pack/plugins/xpack_legacy/kibana.json +++ b/x-pack/plugins/xpack_legacy/kibana.json @@ -1,10 +1,12 @@ { "id": "xpackLegacy", + "owner": { + "name": "Kibana Core", + "githubTeam": "kibana-core" + }, "version": "8.0.0", "kibanaVersion": "kibana", "server": true, "ui": false, - "requiredPlugins": [ - "usageCollection" - ] + "requiredPlugins": ["usageCollection"] } From f243b0540d954a53b1b0371feb22c1cfd5593de9 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 17 Aug 2021 16:21:44 +0200 Subject: [PATCH 11/11] [ML] Check for error messages in the Anomaly Detection jobs health rule type (#108701) * [ML] retrieve job errors * [ML] account for previous execution time * [ML] update default message * [ML] update description * [ML] update unit tests * [ML] update unit tests * [ML] update action name * [ML] update errorMessages name * [ML] update a default message to avoid line breaks * [ML] update rule helper text * [ML] refactor getJobsErrors * [ML] perform errors check starting from the second execution --- x-pack/plugins/ml/common/constants/alerts.ts | 4 +- x-pack/plugins/ml/common/util/alerts.test.ts | 6 + x-pack/plugins/ml/common/util/alerts.ts | 2 +- .../register_jobs_health_alerting_rule.ts | 20 +- .../lib/alerts/jobs_health_service.test.ts | 177 ++++++++++++------ .../server/lib/alerts/jobs_health_service.ts | 54 +++++- .../register_jobs_monitoring_rule_type.ts | 21 ++- .../job_audit_messages/job_audit_messages.ts | 70 ++++++- 8 files changed, 268 insertions(+), 86 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/alerts.ts b/x-pack/plugins/ml/common/constants/alerts.ts index 2192b2b504b59c..1b373b2ec435b3 100644 --- a/x-pack/plugins/ml/common/constants/alerts.ts +++ b/x-pack/plugins/ml/common/constants/alerts.ts @@ -54,12 +54,12 @@ export const HEALTH_CHECK_NAMES: Record { enabled: true, timeInterval: null, }, + errorMessages: { + enabled: true, + }, }); }); test('returns config with overridden values based on provided configuration', () => { @@ -119,6 +122,9 @@ describe('getResultJobsHealthRuleConfig', () => { enabled: true, timeInterval: null, }, + errorMessages: { + enabled: true, + }, }); }); }); diff --git a/x-pack/plugins/ml/common/util/alerts.ts b/x-pack/plugins/ml/common/util/alerts.ts index 7328c2a4dcc711..6abc5333a1f735 100644 --- a/x-pack/plugins/ml/common/util/alerts.ts +++ b/x-pack/plugins/ml/common/util/alerts.ts @@ -54,7 +54,7 @@ export function getTopNBuckets(job: Job): number { return Math.ceil(narrowBucketLength / bucketSpan.asSeconds()); } -const implementedTests = ['datafeed', 'mml', 'delayedData'] as JobsHealthTests[]; +const implementedTests = ['datafeed', 'mml', 'delayedData', 'errorMessages'] as JobsHealthTests[]; /** * Returns tests configuration combined with default values. diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts index f6446b454a8778..a5f433bcc3752b 100644 --- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts @@ -21,7 +21,8 @@ export function registerJobsHealthAlertingRule( triggersActionsUi.ruleTypeRegistry.register({ id: ML_ALERT_TYPES.AD_JOBS_HEALTH, description: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.description', { - defaultMessage: 'Alert when anomaly detection jobs experience operational issues.', + defaultMessage: + 'Alert when anomaly detection jobs experience operational issues. Enable suitable alerts for critically important jobs.', }), iconClass: 'bell', documentationUrl(docLinks) { @@ -90,14 +91,15 @@ export function registerJobsHealthAlertingRule( \\{\\{context.message\\}\\} \\{\\{#context.results\\}\\} Job ID: \\{\\{job_id\\}\\} - \\{\\{#datafeed_id\\}\\}Datafeed ID: \\{\\{datafeed_id\\}\\} \\{\\{/datafeed_id\\}\\} - \\{\\{#datafeed_state\\}\\}Datafeed state: \\{\\{datafeed_state\\}\\} \\{\\{/datafeed_state\\}\\} - \\{\\{#memory_status\\}\\}Memory status: \\{\\{memory_status\\}\\} \\{\\{/memory_status\\}\\} - \\{\\{#log_time\\}\\}Memory logging time: \\{\\{log_time\\}\\} \\{\\{/log_time\\}\\} - \\{\\{#failed_category_count\\}\\}Failed category count: \\{\\{failed_category_count\\}\\} \\{\\{/failed_category_count\\}\\} - \\{\\{#annotation\\}\\}Annotation: \\{\\{annotation\\}\\} \\{\\{/annotation\\}\\} - \\{\\{#missed_docs_count\\}\\}Number of missed documents: \\{\\{missed_docs_count\\}\\} \\{\\{/missed_docs_count\\}\\} - \\{\\{#end_timestamp\\}\\}Latest finalized bucket with missing docs: \\{\\{end_timestamp\\}\\} \\{\\{/end_timestamp\\}\\} + \\{\\{#datafeed_id\\}\\}Datafeed ID: \\{\\{datafeed_id\\}\\} + \\{\\{/datafeed_id\\}\\} \\{\\{#datafeed_state\\}\\}Datafeed state: \\{\\{datafeed_state\\}\\} + \\{\\{/datafeed_state\\}\\} \\{\\{#memory_status\\}\\}Memory status: \\{\\{memory_status\\}\\} + \\{\\{/memory_status\\}\\} \\{\\{#log_time\\}\\}Memory logging time: \\{\\{log_time\\}\\} + \\{\\{/log_time\\}\\} \\{\\{#failed_category_count\\}\\}Failed category count: \\{\\{failed_category_count\\}\\} + \\{\\{/failed_category_count\\}\\} \\{\\{#annotation\\}\\}Annotation: \\{\\{annotation\\}\\} + \\{\\{/annotation\\}\\} \\{\\{#missed_docs_count\\}\\}Number of missed documents: \\{\\{missed_docs_count\\}\\} + \\{\\{/missed_docs_count\\}\\} \\{\\{#end_timestamp\\}\\}Latest finalized bucket with missing docs: \\{\\{end_timestamp\\}\\} + \\{\\{/end_timestamp\\}\\} \\{\\{#errors\\}\\}Error message: \\{\\{message\\}\\} \\{\\{/errors\\}\\} \\{\\{/context.results\\}\\} `, } diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts index b345cf8c1245cb..ffaa26fc949eea 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts @@ -11,9 +11,39 @@ import type { Logger } from 'kibana/server'; import { MlClient } from '../ml_client'; import { MlJob, MlJobStats } from '@elastic/elasticsearch/api/types'; import { AnnotationService } from '../../models/annotation_service/annotation'; +import { JobsHealthExecutorOptions } from './register_jobs_monitoring_rule_type'; +import { JobAuditMessagesService } from '../../models/job_audit_messages/job_audit_messages'; +import { DeepPartial } from '../../../common/types/common'; const MOCK_DATE_NOW = 1487076708000; +function getDefaultExecutorOptions( + overrides: DeepPartial = {} +): JobsHealthExecutorOptions { + return ({ + state: {}, + startedAt: new Date('2021-08-12T13:13:39.396Z'), + previousStartedAt: new Date('2021-08-12T13:13:27.396Z'), + spaceId: 'default', + namespace: undefined, + name: 'ml-health-check', + tags: [], + createdBy: 'elastic', + updatedBy: 'elastic', + rule: { + name: 'ml-health-check', + tags: [], + consumer: 'alerts', + producer: 'ml', + ruleTypeId: 'xpack.ml.anomaly_detection_jobs_health', + ruleTypeName: 'Anomaly detection jobs health', + enabled: true, + schedule: { interval: '10s' }, + }, + ...overrides, + } as unknown) as JobsHealthExecutorOptions; +} + describe('JobsHealthService', () => { const mlClient = ({ getJobs: jest.fn().mockImplementation(({ job_id: jobIds = [] }) => { @@ -117,6 +147,12 @@ describe('JobsHealthService', () => { }), } as unknown) as jest.Mocked; + const jobAuditMessagesService = ({ + getJobsErrors: jest.fn().mockImplementation((jobIds: string) => { + return Promise.resolve({}); + }), + } as unknown) as jest.Mocked; + const logger = ({ warn: jest.fn(), info: jest.fn(), @@ -127,6 +163,7 @@ describe('JobsHealthService', () => { mlClient, datafeedsService, annotationService, + jobAuditMessagesService, logger ); @@ -143,42 +180,52 @@ describe('JobsHealthService', () => { test('returns empty results when no jobs provided', async () => { // act - const executionResult = await jobHealthService.getTestsResults('testRule', { - testsConfig: null, - includeJobs: { - jobIds: ['*'], - groupIds: [], - }, - excludeJobs: null, - }); + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule' }, + params: { + testsConfig: null, + includeJobs: { + jobIds: ['*'], + groupIds: [], + }, + excludeJobs: null, + }, + }) + ); expect(logger.warn).toHaveBeenCalledWith('Rule "testRule" does not have associated jobs.'); expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); expect(executionResult).toEqual([]); }); test('returns empty results and does not perform datafeed check when test is disabled', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule', { - testsConfig: { - datafeed: { - enabled: false, - }, - behindRealtime: null, - delayedData: { - enabled: false, - docsCount: null, - timeInterval: null, - }, - errorMessages: null, - mml: { - enabled: false, + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule' }, + params: { + testsConfig: { + datafeed: { + enabled: false, + }, + behindRealtime: null, + delayedData: { + enabled: false, + docsCount: null, + timeInterval: null, + }, + errorMessages: null, + mml: { + enabled: false, + }, + }, + includeJobs: { + jobIds: ['test_job_01'], + groupIds: [], + }, + excludeJobs: null, }, - }, - includeJobs: { - jobIds: ['test_job_01'], - groupIds: [], - }, - excludeJobs: null, - }); + }) + ); expect(logger.warn).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith(`Performing health checks for job IDs: test_job_01`); expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); @@ -186,27 +233,32 @@ describe('JobsHealthService', () => { }); test('takes into account delayed data params', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule_04', { - testsConfig: { - delayedData: { - enabled: true, - docsCount: 10, - timeInterval: '4h', + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule_04' }, + params: { + testsConfig: { + delayedData: { + enabled: true, + docsCount: 10, + timeInterval: '4h', + }, + behindRealtime: { enabled: false, timeInterval: null }, + mml: { enabled: false }, + datafeed: { enabled: false }, + errorMessages: { enabled: false }, + }, + includeJobs: { + jobIds: [], + groupIds: ['test_group'], + }, + excludeJobs: { + jobIds: ['test_job_03'], + groupIds: [], + }, }, - behindRealtime: { enabled: false, timeInterval: null }, - mml: { enabled: false }, - datafeed: { enabled: false }, - errorMessages: { enabled: false }, - }, - includeJobs: { - jobIds: [], - groupIds: ['test_group'], - }, - excludeJobs: { - jobIds: ['test_job_03'], - groupIds: [], - }, - }); + }) + ); expect(annotationService.getDelayedDataAnnotations).toHaveBeenCalledWith({ jobIds: ['test_job_01', 'test_job_02'], @@ -234,17 +286,22 @@ describe('JobsHealthService', () => { }); test('returns results based on provided selection', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule_03', { - testsConfig: null, - includeJobs: { - jobIds: [], - groupIds: ['test_group'], - }, - excludeJobs: { - jobIds: ['test_job_03'], - groupIds: [], - }, - }); + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule_03' }, + params: { + testsConfig: null, + includeJobs: { + jobIds: [], + groupIds: ['test_group'], + }, + excludeJobs: { + jobIds: ['test_job_03'], + groupIds: [], + }, + }, + }) + ); expect(logger.warn).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith( `Performing health checks for job IDs: test_job_01, test_job_02` diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts index 52e17fed7a4143..bcae57e5585736 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts @@ -11,10 +11,7 @@ import { i18n } from '@kbn/i18n'; import { Logger } from 'kibana/server'; import { MlJob } from '@elastic/elasticsearch/api/types'; import { MlClient } from '../ml_client'; -import { - AnomalyDetectionJobsHealthRuleParams, - JobSelection, -} from '../../routes/schemas/alerting_schema'; +import { JobSelection } from '../../routes/schemas/alerting_schema'; import { datafeedsProvider, DatafeedsService } from '../../models/job_service/datafeeds'; import { ALL_JOBS_SELECTION, HEALTH_CHECK_NAMES } from '../../../common/constants/alerts'; import { DatafeedStats } from '../../../common/types/anomaly_detection_jobs'; @@ -22,6 +19,7 @@ import { GetGuards } from '../../shared_services/shared_services'; import { AnomalyDetectionJobsHealthAlertContext, DelayedDataResponse, + JobsHealthExecutorOptions, MmlTestResponse, NotStartedDatafeedResponse, } from './register_jobs_monitoring_rule_type'; @@ -33,6 +31,10 @@ import { AnnotationService } from '../../models/annotation_service/annotation'; import { annotationServiceProvider } from '../../models/annotation_service'; import { parseInterval } from '../../../common/util/parse_interval'; import { isDefined } from '../../../common/types/guards'; +import { + jobAuditMessagesProvider, + JobAuditMessagesService, +} from '../../models/job_audit_messages/job_audit_messages'; interface TestResult { name: string; @@ -45,6 +47,7 @@ export function jobsHealthServiceProvider( mlClient: MlClient, datafeedsService: DatafeedsService, annotationService: AnnotationService, + jobAuditMessagesService: JobAuditMessagesService, logger: Logger ) { /** @@ -236,13 +239,25 @@ export function jobsHealthServiceProvider( return annotations; }, + /** + * Retrieves a list of the latest errors per jobs. + * @param jobIds List of job IDs. + * @param previousStartedAt Time of the previous rule execution. As we intend to notify + * about an error only once, limit the scope of the errors search. + */ + async getErrorsReport(jobIds: string[], previousStartedAt: Date) { + return await jobAuditMessagesService.getJobsErrors(jobIds, previousStartedAt.getTime()); + }, /** * Retrieves report grouped by test. */ - async getTestsResults( - ruleInstanceName: string, - { testsConfig, includeJobs, excludeJobs }: AnomalyDetectionJobsHealthRuleParams - ): Promise { + async getTestsResults(executorOptions: JobsHealthExecutorOptions): Promise { + const { + rule, + previousStartedAt, + params: { testsConfig, includeJobs, excludeJobs }, + } = executorOptions; + const config = getResultJobsHealthRuleConfig(testsConfig); const results: TestsResults = []; @@ -251,7 +266,7 @@ export function jobsHealthServiceProvider( const jobIds = getJobIds(jobs); if (jobIds.length === 0) { - logger.warn(`Rule "${ruleInstanceName}" does not have associated jobs.`); + logger.warn(`Rule "${rule.name}" does not have associated jobs.`); return results; } @@ -334,6 +349,26 @@ export function jobsHealthServiceProvider( } } + if (config.errorMessages.enabled && previousStartedAt) { + const response = await this.getErrorsReport(jobIds, previousStartedAt); + if (response.length > 0) { + results.push({ + name: HEALTH_CHECK_NAMES.errorMessages.name, + context: { + results: response, + message: i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesMessage', + { + defaultMessage: + '{jobsCount, plural, one {# job contains} other {# jobs contain}} errors in the messages.', + values: { jobsCount: response.length }, + } + ), + }, + }); + } + } + return results; }, }; @@ -360,6 +395,7 @@ export function getJobsHealthServiceProvider(getGuards: GetGuards) { mlClient, datafeedsProvider(scopedClient, mlClient), annotationServiceProvider(scopedClient), + jobAuditMessagesProvider(scopedClient, mlClient), logger ).getTestsResults(...args) ); diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 063d8ad5a89803..c49c169d3bd21d 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -22,6 +22,8 @@ import { AlertInstanceState, AlertTypeState, } from '../../../../alerting/common'; +import { JobsErrorsResponse } from '../../models/job_audit_messages/job_audit_messages'; +import { AlertExecutorOptions } from '../../../../alerting/server'; type ModelSizeStats = MlJobStats['model_size_stats']; @@ -55,7 +57,8 @@ export interface DelayedDataResponse { export type AnomalyDetectionJobHealthResult = | MmlTestResponse | NotStartedDatafeedResponse - | DelayedDataResponse; + | DelayedDataResponse + | JobsErrorsResponse[number]; export type AnomalyDetectionJobsHealthAlertContext = { results: AnomalyDetectionJobHealthResult[]; @@ -69,10 +72,18 @@ export type AnomalyDetectionJobRealtimeIssue = typeof ANOMALY_DETECTION_JOB_REAL export const REALTIME_ISSUE_DETECTED: ActionGroup = { id: ANOMALY_DETECTION_JOB_REALTIME_ISSUE, name: i18n.translate('xpack.ml.jobsHealthAlertingRule.actionGroupName', { - defaultMessage: 'Real-time issue detected', + defaultMessage: 'Issue detected', }), }; +export type JobsHealthExecutorOptions = AlertExecutorOptions< + AnomalyDetectionJobsHealthRuleParams, + Record, + Record, + AnomalyDetectionJobsHealthAlertContext, + AnomalyDetectionJobRealtimeIssue +>; + export function registerJobsMonitoringRuleType({ alerting, mlServicesProviders, @@ -120,14 +131,16 @@ export function registerJobsMonitoringRuleType({ producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, - async executor({ services, params, alertId, state, previousStartedAt, startedAt, name, rule }) { + async executor(options) { + const { services, name } = options; + const fakeRequest = {} as KibanaRequest; const { getTestsResults } = mlServicesProviders.jobsHealthServiceProvider( services.savedObjectsClient, fakeRequest, logger ); - const executionResult = await getTestsResults(name, params); + const executionResult = await getTestsResults(options); if (executionResult.length > 0) { logger.info( diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts index 98ed76319a0f7d..fcda1a2a3ea73a 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts @@ -54,6 +54,10 @@ export function isClearable(index?: string): boolean { return false; } +export type JobsErrorsResponse = Array<{ job_id: string; errors: JobMessage[] }>; + +export type JobAuditMessagesService = ReturnType; + export function jobAuditMessagesProvider( { asInternalUser }: IScopedClusterClient, mlClient: MlClient @@ -178,7 +182,10 @@ export function jobAuditMessagesProvider( return { messages, notificationIndices }; } - // search highest, most recent audit messages for all jobs for the last 24hrs. + /** + * Search highest, most recent audit messages for all jobs for the last 24hrs. + * @param jobIds + */ async function getAuditMessagesSummary(jobIds: string[]): Promise { // TODO This is the current default value of the cluster setting `search.max_buckets`. // This should possibly consider the real settings in a future update. @@ -400,9 +407,70 @@ export function jobAuditMessagesProvider( return (Object.keys(LEVEL) as LevelName[])[Object.values(LEVEL).indexOf(level)]; } + /** + * Retrieve list of errors per job. + * @param jobIds + */ + async function getJobsErrors(jobIds: string[], earliestMs?: number): Promise { + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + size: 0, + body: { + query: { + bool: { + filter: [ + ...(earliestMs ? [{ range: { timestamp: { gte: earliestMs } } }] : []), + { terms: { job_id: jobIds } }, + { + term: { level: { value: MESSAGE_LEVEL.ERROR } }, + }, + ], + }, + }, + aggs: { + by_job: { + terms: { + field: 'job_id', + size: jobIds.length, + }, + aggs: { + latest_errors: { + top_hits: { + size: 10, + sort: [ + { + timestamp: { + order: 'desc', + }, + }, + ], + }, + }, + }, + }, + }, + }, + }); + + const errors = body.aggregations!.by_job as estypes.AggregationsTermsAggregate<{ + key: string; + doc_count: number; + latest_errors: Pick, 'hits'>; + }>; + + return errors.buckets.map((bucket) => { + return { + job_id: bucket.key, + errors: bucket.latest_errors.hits.hits.map((v) => v._source!), + }; + }); + } + return { getJobAuditMessages, getAuditMessagesSummary, clearJobAuditMessages, + getJobsErrors, }; }