diff --git a/package.json b/package.json index 43ab61feba63..1b785b1fcaaa 100644 --- a/package.json +++ b/package.json @@ -271,7 +271,6 @@ "handlebars": "4.7.7", "he": "^1.2.0", "history": "^4.9.0", - "history-extra": "^5.0.1", "hjson": "3.2.1", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts index aac5ec2784ff..6174b9d40e45 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer.ts @@ -30,7 +30,7 @@ export function annotationLayerFunction(): ExpressionFunctionDefinition< help: strings.getAnnotationLayerHideHelp(), }, annotations: { - types: ['manual_event_annotation'], + types: ['manual_point_event_annotation', 'manual_range_event_annotation'], help: strings.getAnnotationLayerAnnotationsHelp(), multi: true, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts index a87c59925f48..539c11854355 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_annotation_layer.ts @@ -30,7 +30,7 @@ export function extendedAnnotationLayerFunction(): ExpressionFunctionDefinition< help: strings.getAnnotationLayerHideHelp(), }, annotations: { - types: ['manual_event_annotation'], + types: ['manual_point_event_annotation', 'manual_range_event_annotation'], help: strings.getAnnotationLayerAnnotationsHelp(), multi: true, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/index.ts b/src/plugins/chart_expressions/expression_xy/common/index.ts index 78c2bf482002..4bee4a3e7f2b 100755 --- a/src/plugins/chart_expressions/expression_xy/common/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/index.ts @@ -41,7 +41,6 @@ export type { AxesSettingsConfig, CommonXYLayerConfig, AnnotationLayerArgs, - XYLayerConfigResult, ExtendedYConfigResult, GridlinesConfigResult, DataLayerConfigResult, diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index cc3f6663f945..b3c7bca93ca2 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -291,10 +291,6 @@ export type XYExtendedLayerConfig = | ExtendedReferenceLineLayerConfig | ExtendedAnnotationLayerConfig; -export type XYLayerConfigResult = - | DataLayerConfigResult - | ReferenceLineLayerConfigResult - | AnnotationLayerConfigResult; export type XYExtendedLayerConfigResult = | ExtendedDataLayerConfigResult | ExtendedReferenceLineLayerConfigResult diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts index 4da90dbb994b..b03ea975b014 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts @@ -7,7 +7,10 @@ */ import { AnnotationTooltipFormatter } from '@elastic/charts'; -import { AvailableAnnotationIcon, EventAnnotationArgs } from '@kbn/event-annotation-plugin/common'; +import { + AvailableAnnotationIcon, + ManualPointEventAnnotationArgs, +} from '@kbn/event-annotation-plugin/common'; import { XY_VIS_RENDERER } from '../constants'; import { XYProps } from './expression_functions'; @@ -21,7 +24,7 @@ export interface XYRender { value: XYChartProps; } -export interface CollectiveConfig extends Omit { +export interface CollectiveConfig extends Omit { roundedTimestamp: number; axisMode: 'bottom'; icon?: AvailableAnnotationIcon | string; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index 5197e54cbe1c..1b42184affdb 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`XYChart component annotations should render basic annotation 1`] = ` +exports[`XYChart component annotations should render basic line annotation 1`] = ` `; -exports[`XYChart component annotations should render grouped annotations preserving the shared styles 1`] = ` +exports[`XYChart component annotations should render basic range annotation 1`] = ` +Array [ + , + , +] +`; + +exports[`XYChart component annotations should render grouped line annotations preserving the shared styles 1`] = ` `; -exports[`XYChart component annotations should render grouped annotations with default styles 1`] = ` +exports[`XYChart component annotations should render grouped line annotations with default styles 1`] = ` `; -exports[`XYChart component annotations should render simplified annotation when hide is true 1`] = ` +exports[`XYChart component annotations should render simplified annotations when hide is true 1`] = ` - } - markerBody={ - - } markerPosition="top" style={ Object { @@ -213,6 +236,50 @@ exports[`XYChart component annotations should render simplified annotation when /> `; +exports[`XYChart component annotations should render simplified annotations when hide is true 2`] = ` +Array [ + , + , +] +`; + exports[`XYChart component it renders area 1`] = ` >; hide?: boolean; minInterval?: number; isBarChart?: boolean; + outsideDimension: number; } const groupVisibleConfigsByInterval = ( @@ -53,9 +64,11 @@ const groupVisibleConfigsByInterval = ( firstTimestamp?: number ) => { return layers - .flatMap(({ annotations }) => annotations.filter((a) => !a.isHidden)) + .flatMap(({ annotations }) => + annotations.filter((a) => !a.isHidden && a.type === 'manual_point_event_annotation') + ) .sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf()) - .reduce>((acc, current) => { + .reduce>((acc, current) => { const roundedTimestamp = getRoundedTimestamp( moment(current.time).valueOf(), firstTimestamp, @@ -70,7 +83,7 @@ const groupVisibleConfigsByInterval = ( const createCustomTooltipDetails = ( - config: EventAnnotationArgs[], + config: ManualPointEventAnnotationArgs[], formatter?: FieldFormat ): AnnotationTooltipFormatter | undefined => () => { @@ -93,8 +106,8 @@ const createCustomTooltipDetails = ); }; -function getCommonProperty( - configArr: EventAnnotationArgs[], +function getCommonProperty( + configArr: ManualPointEventAnnotationArgs[], propertyName: K, fallbackValue: T ) { @@ -105,9 +118,9 @@ function getCommonProperty( return fallbackValue; } -const getCommonStyles = (configArr: EventAnnotationArgs[]) => { +const getCommonStyles = (configArr: ManualPointEventAnnotationArgs[]) => { return { - color: getCommonProperty( + color: getCommonProperty( configArr, 'color', defaultAnnotationColor @@ -118,6 +131,20 @@ const getCommonStyles = (configArr: EventAnnotationArgs[]) => { }; }; +export const getRangeAnnotations = (layers: AnnotationLayerConfigResult[]) => { + return layers + .flatMap(({ annotations }) => + annotations.filter( + (a): a is ManualRangeEventAnnotationOutput => + a.type === 'manual_range_event_annotation' && !a.isHidden + ) + ) + .sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf()); +}; + +export const OUTSIDE_RECT_ANNOTATION_WIDTH = 8; +export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2; + export const getAnnotationsGroupedByInterval = ( layers: CommonXYAnnotationLayerConfig[], minInterval?: number, @@ -145,18 +172,23 @@ export const getAnnotationsGroupedByInterval = ( }); }; +// todo: remove when closed https://github.com/elastic/elastic-charts/issues/1647 +RectAnnotation.displayName = 'RectAnnotation'; + export const Annotations = ({ - groupedAnnotations, + groupedLineAnnotations, + rangeAnnotations, formatter, isHorizontal, paddingMap, hide, minInterval, isBarChart, + outsideDimension, }: AnnotationsProps) => { return ( <> - {groupedAnnotations.map((annotation) => { + {groupedLineAnnotations.map((annotation) => { const markerPositionVertical = Position.Top; const markerPosition = isHorizontal ? mapVerticalToHorizontalPlacement(markerPositionVertical) @@ -227,6 +259,40 @@ export const Annotations = ({ /> ); })} + {rangeAnnotations.map(({ label, time, color, endTime, outside }) => { + const id = snakeCase(label); + + return ( + ( +
+ +

+ {formatter + ? `${formatter.convert(time)} — ${formatter?.convert(endTime)}` + : `${moment(time).toISOString()} — ${moment(endTime).toISOString()}`} +

+
+
{label}
+
+ )} + dataValues={[ + { + coordinates: { + x0: moment(time).valueOf(), + x1: moment(endTime).valueOf(), + }, + details: label, + }, + ]} + style={{ fill: color || defaultAnnotationRangeColor, opacity: 1 }} + outside={Boolean(outside)} + outsideDimension={outsideDimension} + /> + ); + })} ); }; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index eb0b87bf6c9f..1ad7f8698459 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -20,6 +20,7 @@ import { LineAnnotation, LineSeries, Position, + RectAnnotation, ScaleType, SeriesNameFn, Settings, @@ -30,7 +31,7 @@ import { Datatable } from '@kbn/expressions-plugin/common'; import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common'; -import { CommonXYAnnotationLayerConfig, DataLayerConfig } from '../../common'; +import { DataLayerConfig } from '../../common'; import { LayerTypes } from '../../common/constants'; import { XyEndzones } from './x_domain'; import { @@ -49,8 +50,14 @@ import { sampleLayer, } from '../../common/__mocks__'; import { XYChart, XYChartRenderProps } from './xy_chart'; -import { ExtendedDataLayerConfig, XYChartProps, XYProps } from '../../common/types'; +import { + AnnotationLayerConfigResult, + ExtendedDataLayerConfig, + XYChartProps, + XYProps, +} from '../../common/types'; import { DataLayers } from './data_layers'; +import { Annotations } from './annotations'; const onClickValue = jest.fn(); const onSelectRange = jest.fn(); @@ -2522,31 +2529,36 @@ describe('XYChart component', () => { }); describe('annotations', () => { - const sampleStyledAnnotation: EventAnnotationOutput = { + const customLineStaticAnnotation: EventAnnotationOutput = { time: '2022-03-18T08:25:00.000Z', label: 'Event 1', icon: 'triangle', - type: 'manual_event_annotation', + type: 'manual_point_event_annotation' as const, color: 'red', lineStyle: 'dashed', lineWidth: 3, }; - const sampleAnnotationLayers: CommonXYAnnotationLayerConfig[] = [ - { - layerId: 'annotationLayer', - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - annotations: [ - { - time: '2022-03-18T08:25:17.140Z', - label: 'Annotation', - type: 'manual_event_annotation', - }, - ], - }, - ]; - function sampleArgsWithAnnotation(annotationLayers = sampleAnnotationLayers): XYChartProps { + const defaultLineStaticAnnotation = { + time: '2022-03-18T08:25:17.140Z', + label: 'Annotation', + type: 'manual_point_event_annotation' as const, + }; + const defaultRangeStaticAnnotation = { + time: '2022-03-18T08:25:17.140Z', + endTime: '2022-03-31T08:25:17.140Z', + label: 'Event range', + type: 'manual_range_event_annotation' as const, + }; + const createLayerWithAnnotations = ( + annotations: EventAnnotationOutput[] = [defaultLineStaticAnnotation] + ): AnnotationLayerConfigResult => ({ + type: 'annotationLayer', + layerType: LayerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations, + }); + function sampleArgsWithAnnotations(annotationLayers = [createLayerWithAnnotations()]) { const { args } = sampleArgs(); return { args: { @@ -2555,34 +2567,40 @@ describe('XYChart component', () => { }, }; } - test('should render basic annotation', () => { - const { args } = sampleArgsWithAnnotation(); + + test('should render basic line annotation', () => { + const { args } = sampleArgsWithAnnotations(); const component = mount(); expect(component.find('LineAnnotation')).toMatchSnapshot(); }); - test('should render simplified annotation when hide is true', () => { - const { args } = sampleArgsWithAnnotation(); - (args.layers[0] as CommonXYAnnotationLayerConfig).hide = true; + test('should render basic range annotation', () => { + const { args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([defaultLineStaticAnnotation, defaultRangeStaticAnnotation]), + ]); + const component = mount(); + expect(component.find(RectAnnotation)).toMatchSnapshot(); + }); + test('should render simplified annotations when hide is true', () => { + const { args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([defaultLineStaticAnnotation, defaultRangeStaticAnnotation]), + ]); + (args.layers[1] as AnnotationLayerConfigResult).hide = true; const component = mount(); expect(component.find('LineAnnotation')).toMatchSnapshot(); + expect(component.find('RectAnnotation')).toMatchSnapshot(); }); - test('should render grouped annotations preserving the shared styles', () => { - const { args } = sampleArgsWithAnnotation([ - { - layerId: 'annotationLayer', - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - annotations: [ - sampleStyledAnnotation, - { ...sampleStyledAnnotation, time: '2022-03-18T08:25:00.020Z', label: 'Event 2' }, - { - ...sampleStyledAnnotation, - time: '2022-03-18T08:25:00.001Z', - label: 'Event 3', - }, - ], - }, + test('should render grouped line annotations preserving the shared styles', () => { + const { args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([ + customLineStaticAnnotation, + { ...customLineStaticAnnotation, time: '2022-03-18T08:25:00.020Z', label: 'Event 2' }, + { + ...customLineStaticAnnotation, + time: '2022-03-18T08:25:00.001Z', + label: 'Event 3', + }, + ]), ]); const component = mount(); const groupedAnnotation = component.find(LineAnnotation); @@ -2602,30 +2620,21 @@ describe('XYChart component', () => { ' Event 1 2022-03-18T08:25:00.000Z Event 3 2022-03-18T08:25:00.001Z Event 2 2022-03-18T08:25:00.020Z' ); }); - test('should render grouped annotations with default styles', () => { - const { args } = sampleArgsWithAnnotation([ - { - layerId: 'annotationLayer', - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - annotations: [sampleStyledAnnotation], - }, - { - layerId: 'annotationLayer2', - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - annotations: [ - { - ...sampleStyledAnnotation, - icon: 'asterisk', - color: 'blue', - lineStyle: 'dotted', - lineWidth: 10, - time: '2022-03-18T08:25:00.001Z', - label: 'Event 2', - }, - ], - }, + + test('should render grouped line annotations with default styles', () => { + const { args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([customLineStaticAnnotation]), + createLayerWithAnnotations([ + { + ...customLineStaticAnnotation, + icon: 'triangle' as const, + color: 'blue', + lineStyle: 'dotted', + lineWidth: 10, + time: '2022-03-18T08:25:00.001Z', + label: 'Event 2', + }, + ]), ]); const component = mount(); const groupedAnnotation = component.find(LineAnnotation); @@ -2635,27 +2644,26 @@ describe('XYChart component', () => { expect(groupedAnnotation).toMatchSnapshot(); }); test('should not render hidden annotations', () => { - const { args } = sampleArgsWithAnnotation([ - { - layerId: 'annotationLayer', - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - annotations: [ - sampleStyledAnnotation, - { ...sampleStyledAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' }, - { - ...sampleStyledAnnotation, - time: '2022-03-18T08:35:00.001Z', - label: 'Event 3', - isHidden: true, - }, - ], - }, + const { args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([ + customLineStaticAnnotation, + { ...customLineStaticAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' }, + { + ...customLineStaticAnnotation, + time: '2022-03-18T08:35:00.001Z', + label: 'Event 3', + isHidden: true, + }, + defaultRangeStaticAnnotation, + { ...defaultRangeStaticAnnotation, label: 'range', isHidden: true }, + ]), ]); const component = mount(); - const annotations = component.find(LineAnnotation); + const lineAnnotations = component.find(LineAnnotation); + const rectAnnotations = component.find(Annotations).find(RectAnnotation); - expect(annotations.length).toEqual(2); + expect(lineAnnotations.length).toEqual(2); + expect(rectAnnotations.length).toEqual(1); }); }); }); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index ac4045a576ff..93ddf56f419f 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -55,10 +55,15 @@ import { getLegendAction } from './legend_action'; import { ReferenceLineAnnotations, computeChartMargins } from './reference_lines'; import { visualizationDefinitions } from '../definitions'; import { CommonXYLayerConfig } from '../../common/types'; -import { Annotations, getAnnotationsGroupedByInterval } from './annotations'; +import { + Annotations, + getAnnotationsGroupedByInterval, + getRangeAnnotations, + OUTSIDE_RECT_ANNOTATION_WIDTH, + OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION, +} from './annotations'; import { AxisExtentModes, SeriesTypes, ValueLabelModes } from '../../common/constants'; import { DataLayers } from './data_layers'; - import './xy_chart.scss'; declare global { @@ -243,18 +248,21 @@ export function XYChart({ const xColumnId = firstTable.columns.find((col) => col.id === dataLayers[0]?.xAccessor)?.id; - const groupedAnnotations = getAnnotationsGroupedByInterval( + const groupedLineAnnotations = getAnnotationsGroupedByInterval( annotationsLayers, minInterval, xColumnId ? firstTable.rows[0]?.[xColumnId] : undefined, xAxisFormatter ); + const rangeAnnotations = getRangeAnnotations(annotationsLayers); + const visualConfigs = [ ...referenceLineLayers.flatMap(({ yConfig }) => yConfig), - ...groupedAnnotations, + ...groupedLineAnnotations, ].filter(Boolean); - const linesPaddings = getLinesCausedPaddings(visualConfigs, yAxesMap); + const shouldHideDetails = annotationsLayers.length > 0 ? annotationsLayers[0].hide : false; + const linesPaddings = !shouldHideDetails ? getLinesCausedPaddings(visualConfigs, yAxesMap) : {}; const getYAxesStyle = (groupId: 'left' | 'right') => { const tickVisible = @@ -622,15 +630,24 @@ export function XYChart({ paddingMap={linesPaddings} /> ) : null} - {groupedAnnotations.length ? ( + {rangeAnnotations.length || groupedLineAnnotations.length ? ( 0} minInterval={minInterval} + hide={annotationsLayers?.[0].hide} + outsideDimension={ + rangeAnnotations.length && shouldHideDetails + ? OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION + : shouldUseNewTimeAxis + ? Number(MULTILAYER_TIME_AXIS_STYLE.tickLine?.padding || 0) + + Number(chartTheme.axes?.tickLabel?.fontSize || 0) + : Number(chartTheme.axes?.tickLine?.size) || OUTSIDE_RECT_ANNOTATION_WIDTH + } /> ) : null}
diff --git a/src/plugins/controls/public/plugin.ts b/src/plugins/controls/public/plugin.ts index 51705640b06f..9b0d754b3f15 100644 --- a/src/plugins/controls/public/plugin.ts +++ b/src/plugins/controls/public/plugin.ts @@ -30,12 +30,14 @@ import { CONTROL_GROUP_TYPE, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, - TIME_SLIDER_CONTROL, + // TIME_SLIDER_CONTROL, } from '.'; +/* import { TimesliderEmbeddableFactory, TimeSliderControlEmbeddableInput, } from './control_types/time_slider'; +*/ import { controlsService } from './services/kibana/controls'; export class ControlsPlugin @@ -104,6 +106,7 @@ export class ControlsPlugin registerControlType(rangeSliderFactory); // Time Slider Control Factory Setup + /* Temporary disabling Time Slider const timeSliderFactoryDef = new TimesliderEmbeddableFactory(); const timeSliderFactory = embeddable.registerEmbeddableFactory( TIME_SLIDER_CONTROL, @@ -113,8 +116,10 @@ export class ControlsPlugin timeSliderFactoryDef, timeSliderFactory ); + registerControlType(timeSliderFactory); + */ }); return { diff --git a/src/plugins/controls/server/plugin.ts b/src/plugins/controls/server/plugin.ts index cbe9d3923436..fb39acfaf913 100644 --- a/src/plugins/controls/server/plugin.ts +++ b/src/plugins/controls/server/plugin.ts @@ -14,7 +14,7 @@ import { PluginSetup as UnifiedSearchSetup } from '@kbn/unified-search-plugin/se import { setupOptionsListSuggestionsRoute } from './control_types/options_list/options_list_suggestions_route'; import { controlGroupContainerPersistableStateServiceFactory } from './control_group/control_group_container_factory'; import { optionsListPersistableStateServiceFactory } from './control_types/options_list/options_list_embeddable_factory'; -import { timeSliderPersistableStateServiceFactory } from './control_types/time_slider/time_slider_embeddable_factory'; +// import { timeSliderPersistableStateServiceFactory } from './control_types/time_slider/time_slider_embeddable_factory'; interface SetupDeps { embeddable: EmbeddableSetup; @@ -25,7 +25,8 @@ interface SetupDeps { export class ControlsPlugin implements Plugin { public setup(core: CoreSetup, { embeddable, unifiedSearch }: SetupDeps) { embeddable.registerEmbeddableFactory(optionsListPersistableStateServiceFactory()); - embeddable.registerEmbeddableFactory(timeSliderPersistableStateServiceFactory()); + // Temporary disabling Time Slider + // embeddable.registerEmbeddableFactory(timeSliderPersistableStateServiceFactory()); embeddable.registerEmbeddableFactory( controlGroupContainerPersistableStateServiceFactory(embeddable) diff --git a/src/plugins/data_view_editor/public/components/data_view_editor.tsx b/src/plugins/data_view_editor/public/components/data_view_editor.tsx index 18af3c4ebd6d..e09acfaca4d5 100644 --- a/src/plugins/data_view_editor/public/components/data_view_editor.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_editor.tsx @@ -23,6 +23,7 @@ export const DataViewEditor = ({ services, defaultTypeIsRollup = false, requireTimestampField = false, + showEmptyPrompt = true, }: DataViewEditorPropsWithServices) => { const { Provider: KibanaReactContextProvider } = createKibanaReactContext(services); @@ -35,6 +36,7 @@ export const DataViewEditor = ({ onCancel={onCancel} defaultTypeIsRollup={defaultTypeIsRollup} requireTimestampField={requireTimestampField} + showEmptyPrompt={showEmptyPrompt} /> diff --git a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx index 9d21af4b2df0..9cdfad745bea 100644 --- a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -58,6 +58,7 @@ export interface Props { onCancel: () => void; defaultTypeIsRollup?: boolean; requireTimestampField?: boolean; + showEmptyPrompt?: boolean; } const editorTitle = i18n.translate('indexPatternEditor.title', { @@ -69,6 +70,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ onCancel, defaultTypeIsRollup, requireTimestampField = false, + showEmptyPrompt = true, }: Props) => { const { services: { http, dataViews, uiSettings, searchClient }, @@ -316,7 +318,12 @@ const IndexPatternEditorFlyoutContentComponent = ({ ); return ( - + diff --git a/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx b/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx index 2fe95d753bb0..dd6d474068c2 100644 --- a/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx @@ -18,6 +18,7 @@ const IndexPatternFlyoutContentContainer = ({ onCancel = () => {}, defaultTypeIsRollup, requireTimestampField = false, + showEmptyPrompt = true, }: DataViewEditorProps) => { const { services: { dataViews, notifications }, @@ -48,6 +49,7 @@ const IndexPatternFlyoutContentContainer = ({ onCancel={onCancel} defaultTypeIsRollup={defaultTypeIsRollup} requireTimestampField={requireTimestampField} + showEmptyPrompt={showEmptyPrompt} /> ); }; diff --git a/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx b/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx index 686decce9c65..ecfdd9e5c1c9 100644 --- a/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx +++ b/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx @@ -27,6 +27,7 @@ interface Props { onCancel: () => void; allSources: MatchedItem[]; loadSources: () => void; + showEmptyPrompt?: boolean; } export function isUserDataIndex(source: MatchedItem) { @@ -45,7 +46,13 @@ export function isUserDataIndex(source: MatchedItem) { return true; } -export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSources }) => { +export const EmptyPrompts: FC = ({ + allSources, + onCancel, + children, + loadSources, + showEmptyPrompt, +}) => { const { services: { docLinks, application, http, searchClient, dataViews }, } = useKibana(); @@ -93,7 +100,7 @@ export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSo ); - } else { + } else if (showEmptyPrompt) { // first time return ( <> @@ -108,6 +115,8 @@ export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSo ); + } else { + setGoToForm(true); } } diff --git a/src/plugins/data_view_editor/public/open_editor.tsx b/src/plugins/data_view_editor/public/open_editor.tsx index 7fd5163edacc..ed57870c4978 100644 --- a/src/plugins/data_view_editor/public/open_editor.tsx +++ b/src/plugins/data_view_editor/public/open_editor.tsx @@ -49,6 +49,7 @@ export const getEditorOpener = onCancel = () => {}, defaultTypeIsRollup = false, requireTimestampField = false, + showEmptyPrompt = true, }: DataViewEditorProps): CloseEditor => { const closeEditor = () => { if (overlayRef) { @@ -77,6 +78,7 @@ export const getEditorOpener = }} defaultTypeIsRollup={defaultTypeIsRollup} requireTimestampField={requireTimestampField} + showEmptyPrompt={showEmptyPrompt} /> , diff --git a/src/plugins/data_view_editor/public/types.ts b/src/plugins/data_view_editor/public/types.ts index fe6928ee7373..a2d359ba8420 100644 --- a/src/plugins/data_view_editor/public/types.ts +++ b/src/plugins/data_view_editor/public/types.ts @@ -49,6 +49,11 @@ export interface DataViewEditorProps { * Sets whether a timestamp field is required to create an index pattern. Defaults to false. */ requireTimestampField?: boolean; + /** + * If set to false, the screen for prompting a user to create a data view will be skipped, and the user will be taken directly + * to data view creation. + */ + showEmptyPrompt?: boolean; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/plugins/event_annotation/common/event_annotation_group/index.ts b/src/plugins/event_annotation/common/event_annotation_group/index.ts index f6a1f38459c1..a3a36505b1c2 100644 --- a/src/plugins/event_annotation/common/event_annotation_group/index.ts +++ b/src/plugins/event_annotation/common/event_annotation_group/index.ts @@ -35,7 +35,7 @@ export function eventAnnotationGroup(): ExpressionFunctionDefinition< }), args: { annotations: { - types: ['manual_event_annotation'], + types: ['manual_point_event_annotation', 'manual_range_event_annotation'], help: i18n.translate('eventAnnotation.group.args.annotationConfigs', { defaultMessage: 'Annotation configs', }), diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index f3421582d01b..30d0d69a0f6c 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -6,8 +6,15 @@ * Side Public License, v 1. */ -export type { EventAnnotationArgs, EventAnnotationOutput } from './manual_event_annotation/types'; -export { manualEventAnnotation } from './manual_event_annotation'; +export type { + EventAnnotationArgs, + EventAnnotationOutput, + ManualPointEventAnnotationArgs, + ManualPointEventAnnotationOutput, + ManualRangeEventAnnotationArgs, + ManualRangeEventAnnotationOutput, +} from './manual_event_annotation/types'; +export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation'; export { eventAnnotationGroup } from './event_annotation_group'; export type { EventAnnotationGroupArgs } from './event_annotation_group'; -export type { EventAnnotationConfig, AvailableAnnotationIcon } from './types'; +export type { EventAnnotationConfig, RangeEventAnnotationConfig, AvailableAnnotationIcon } from './types'; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/index.ts b/src/plugins/event_annotation/common/manual_event_annotation/index.ts index 73447e3b6c26..bb02018f5a81 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/index.ts @@ -8,18 +8,24 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; -import type { EventAnnotationArgs, EventAnnotationOutput } from './types'; import { AvailableAnnotationIcons } from '../constants'; -export const manualEventAnnotation: ExpressionFunctionDefinition< - 'manual_event_annotation', +import type { + ManualRangeEventAnnotationArgs, + ManualRangeEventAnnotationOutput, + ManualPointEventAnnotationArgs, + ManualPointEventAnnotationOutput, +} from './types'; + +export const manualPointEventAnnotation: ExpressionFunctionDefinition< + 'manual_point_event_annotation', null, - EventAnnotationArgs, - EventAnnotationOutput + ManualPointEventAnnotationArgs, + ManualPointEventAnnotationOutput > = { - name: 'manual_event_annotation', + name: 'manual_point_event_annotation', aliases: [], - type: 'manual_event_annotation', + type: 'manual_point_event_annotation', help: i18n.translate('eventAnnotation.manualAnnotation.description', { defaultMessage: `Configure manual annotation`, }), @@ -77,9 +83,68 @@ export const manualEventAnnotation: ExpressionFunctionDefinition< }), }, }, - fn(input: unknown, args: EventAnnotationArgs) { + fn: function fn(input: unknown, args: ManualPointEventAnnotationArgs) { + return { + type: 'manual_point_event_annotation', + ...args, + }; + }, +}; + +export const manualRangeEventAnnotation: ExpressionFunctionDefinition< + 'manual_range_event_annotation', + null, + ManualRangeEventAnnotationArgs, + ManualRangeEventAnnotationOutput +> = { + name: 'manual_range_event_annotation', + aliases: [], + type: 'manual_range_event_annotation', + help: i18n.translate('eventAnnotation.manualAnnotation.description', { + defaultMessage: `Configure manual annotation`, + }), + inputTypes: ['null'], + args: { + time: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.time', { + defaultMessage: `Timestamp for annotation`, + }), + }, + endTime: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.endTime', { + defaultMessage: `Timestamp for range annotation`, + }), + required: false, + }, + outside: { + types: ['boolean'], + help: '', + required: false, + }, + label: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.label', { + defaultMessage: `The name of the annotation`, + }), + }, + color: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.color', { + defaultMessage: 'The color of the line', + }), + }, + isHidden: { + types: ['boolean'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.isHidden', { + defaultMessage: `Switch to hide annotation`, + }), + }, + }, + fn: function fn(input: unknown, args: ManualRangeEventAnnotationArgs) { return { - type: 'manual_event_annotation', + type: 'manual_range_event_annotation', ...args, }; }, diff --git a/src/plugins/event_annotation/common/manual_event_annotation/types.ts b/src/plugins/event_annotation/common/manual_event_annotation/types.ts index e1bed4a592d2..208383734924 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/types.ts @@ -6,10 +6,26 @@ * Side Public License, v 1. */ -import { StyleProps } from '../types'; +import { PointStyleProps, RangeStyleProps } from '../types'; -export type EventAnnotationArgs = { +export type ManualPointEventAnnotationArgs = { time: string; -} & StyleProps; +} & PointStyleProps; -export type EventAnnotationOutput = EventAnnotationArgs & { type: 'manual_event_annotation' }; +export type ManualPointEventAnnotationOutput = ManualPointEventAnnotationArgs & { + type: 'manual_point_event_annotation'; +}; + +export type ManualRangeEventAnnotationArgs = { + time: string; + endTime: string; +} & RangeStyleProps; + +export type ManualRangeEventAnnotationOutput = ManualRangeEventAnnotationArgs & { + type: 'manual_range_event_annotation'; +}; + +export type EventAnnotationArgs = ManualPointEventAnnotationArgs | ManualRangeEventAnnotationArgs; +export type EventAnnotationOutput = + | ManualPointEventAnnotationOutput + | ManualRangeEventAnnotationOutput; diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 634547274204..e0b0de3c85c9 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -10,11 +10,11 @@ import { $Values } from '@kbn/utility-types'; import { AvailableAnnotationIcons } from './constants'; export type LineStyle = 'solid' | 'dashed' | 'dotted'; +export type Fill = 'inside' | 'outside' | 'none'; export type AnnotationType = 'manual'; -export type KeyType = 'point_in_time'; +export type KeyType = 'point_in_time' | 'range'; export type AvailableAnnotationIcon = $Values; - -export interface StyleProps { +export interface PointStyleProps { label: string; color?: string; icon?: AvailableAnnotationIcon; @@ -24,10 +24,30 @@ export interface StyleProps { isHidden?: boolean; } -export type EventAnnotationConfig = { +export type PointInTimeEventAnnotationConfig = { id: string; key: { - type: KeyType; + type: 'point_in_time'; timestamp: string; }; -} & StyleProps; +} & PointStyleProps; + +export interface RangeStyleProps { + label: string; + color?: string; + outside?: boolean; + isHidden?: boolean; +} + +export type RangeEventAnnotationConfig = { + id: string; + key: { + type: 'range'; + timestamp: string; + endTimestamp: string; + }; +} & RangeStyleProps; + +export type StyleProps = PointStyleProps & RangeStyleProps; + +export type EventAnnotationConfig = PointInTimeEventAnnotationConfig | RangeEventAnnotationConfig; diff --git a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts index aed33da84057..8eb3d05309ec 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts @@ -5,5 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import { euiLightVars } from '@kbn/ui-theme'; +import { EventAnnotationConfig, RangeEventAnnotationConfig } from '../../common'; export const defaultAnnotationColor = euiLightVars.euiColorAccent; +export const defaultAnnotationRangeColor = `#F04E981A`; // defaultAnnotationColor with opacity 0.1 + +export const defaultAnnotationLabel = i18n.translate( + 'eventAnnotation.manualAnnotation.defaultAnnotationLabel', + { + defaultMessage: 'Event', + } +); + +export const isRangeAnnotation = ( + annotation?: EventAnnotationConfig +): annotation is RangeEventAnnotationConfig => { + return Boolean(annotation && annotation?.key.type === 'range'); +}; diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index 3d81ea6a3e3a..4770c1c182af 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -7,43 +7,70 @@ */ import { EventAnnotationServiceType } from './types'; -import { defaultAnnotationColor } from './helpers'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + defaultAnnotationLabel, +} from './helpers'; +import { EventAnnotationConfig } from '../../common'; +import { RangeEventAnnotationConfig } from '../../common/types'; export function hasIcon(icon: string | undefined): icon is string { return icon != null && icon !== 'empty'; } +const isRangeAnnotation = ( + annotation?: EventAnnotationConfig +): annotation is RangeEventAnnotationConfig => { + return Boolean(annotation && annotation?.key.type === 'range'); +}; + export function getEventAnnotationService(): EventAnnotationServiceType { return { - toExpression: ({ - label, - isHidden, - color, - lineStyle, - lineWidth, - icon, - textVisibility, - time, - }) => { - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'manual_event_annotation', - arguments: { - time: [time], - label: [label], - color: [color || defaultAnnotationColor], - lineWidth: [lineWidth || 1], - lineStyle: [lineStyle || 'solid'], - icon: hasIcon(icon) ? [icon] : ['triangle'], - textVisibility: [textVisibility || false], - isHidden: [Boolean(isHidden)], + toExpression: (annotation) => { + if (isRangeAnnotation(annotation)) { + const { label, isHidden, color, key, outside } = annotation; + const { timestamp: time, endTimestamp: endTime } = key; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'manual_range_event_annotation', + arguments: { + time: [time], + endTime: [endTime], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationRangeColor], + outside: [Boolean(outside)], + isHidden: [Boolean(isHidden)], + }, + }, + ], + }; + } else { + const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility } = + annotation; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'manual_point_event_annotation', + arguments: { + time: [key.timestamp], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationColor], + lineWidth: [lineWidth || 1], + lineStyle: [lineStyle || 'solid'], + icon: hasIcon(icon) ? [icon] : ['triangle'], + textVisibility: [textVisibility || false], + isHidden: [Boolean(isHidden)], + }, }, - }, - ], - }; + ], + }; + } }, }; } diff --git a/src/plugins/event_annotation/public/event_annotation_service/types.ts b/src/plugins/event_annotation/public/event_annotation_service/types.ts index c44b2d1e536d..d5fcaa23107c 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/types.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/types.ts @@ -7,8 +7,8 @@ */ import { ExpressionAstExpression } from '@kbn/expressions-plugin/common/ast'; -import { EventAnnotationArgs } from '../../common'; +import { EventAnnotationConfig } from '../../common'; export interface EventAnnotationServiceType { - toExpression: (props: EventAnnotationArgs) => ExpressionAstExpression; + toExpression: (props: EventAnnotationConfig) => ExpressionAstExpression; } diff --git a/src/plugins/event_annotation/public/index.ts b/src/plugins/event_annotation/public/index.ts index c15429c94cbe..56ddc4b8a60e 100644 --- a/src/plugins/event_annotation/public/index.ts +++ b/src/plugins/event_annotation/public/index.ts @@ -14,4 +14,8 @@ export const plugin = () => new EventAnnotationPlugin(); export type { EventAnnotationPluginSetup, EventAnnotationPluginStart } from './plugin'; export * from './event_annotation_service/types'; export { EventAnnotationService } from './event_annotation_service'; -export { defaultAnnotationColor } from './event_annotation_service/helpers'; +export { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotation, +} from './event_annotation_service/helpers'; diff --git a/src/plugins/event_annotation/public/plugin.ts b/src/plugins/event_annotation/public/plugin.ts index f3f4fcfcc60f..9314151375f2 100644 --- a/src/plugins/event_annotation/public/plugin.ts +++ b/src/plugins/event_annotation/public/plugin.ts @@ -8,7 +8,11 @@ import { Plugin, CoreSetup } from '@kbn/core/public'; import { ExpressionsSetup } from '@kbn/expressions-plugin/public'; -import { manualEventAnnotation, eventAnnotationGroup } from '../common'; +import { + manualPointEventAnnotation, + manualRangeEventAnnotation, + eventAnnotationGroup, +} from '../common'; import { EventAnnotationService } from './event_annotation_service'; interface SetupDependencies { @@ -28,7 +32,8 @@ export class EventAnnotationPlugin private readonly eventAnnotationService = new EventAnnotationService(); public setup(core: CoreSetup, dependencies: SetupDependencies): EventAnnotationPluginSetup { - dependencies.expressions.registerFunction(manualEventAnnotation); + dependencies.expressions.registerFunction(manualPointEventAnnotation); + dependencies.expressions.registerFunction(manualRangeEventAnnotation); dependencies.expressions.registerFunction(eventAnnotationGroup); return this.eventAnnotationService; } diff --git a/src/plugins/event_annotation/server/plugin.ts b/src/plugins/event_annotation/server/plugin.ts index 0643611af9bb..387326fcf2a2 100644 --- a/src/plugins/event_annotation/server/plugin.ts +++ b/src/plugins/event_annotation/server/plugin.ts @@ -8,7 +8,11 @@ import { CoreSetup, Plugin } from '@kbn/core/server'; import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server'; -import { manualEventAnnotation, eventAnnotationGroup } from '../common'; +import { + manualPointEventAnnotation, + eventAnnotationGroup, + manualRangeEventAnnotation, +} from '../common'; interface SetupDependencies { expressions: ExpressionsServerSetup; @@ -16,7 +20,8 @@ interface SetupDependencies { export class EventAnnotationServerPlugin implements Plugin { public setup(core: CoreSetup, dependencies: SetupDependencies) { - dependencies.expressions.registerFunction(manualEventAnnotation); + dependencies.expressions.registerFunction(manualPointEventAnnotation); + dependencies.expressions.registerFunction(manualRangeEventAnnotation); dependencies.expressions.registerFunction(eventAnnotationGroup); return {}; diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index ec3852b309d3..d462a89108c0 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -111,6 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.selectAggType('derivative', 1); await visualBuilder.setFieldForAggregation('Max of machine.ram', 1); + await visChart.waitForVisualizationRenderingStabilized(); const value = await visualBuilder.getMetricValue(); expect(value).to.eql('0'); @@ -197,6 +198,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.selectAggType('derivative', 1); await visualBuilder.setFieldForAggregation('Max of machine.ram', 1); + await visChart.waitForVisualizationRenderingStabilized(); const value = await visualBuilder.getGaugeCount(); expect(value).to.eql('0'); @@ -226,6 +228,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.clickSeriesOption(); await visualBuilder.changeDataFormatter('number'); + await visChart.waitForVisualizationRenderingStabilized(); const gaugeLabel = await visualBuilder.getGaugeLabel(); const gaugeCount = await visualBuilder.getGaugeCount(); @@ -239,6 +242,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.clickPanelOptions('gauge'); await visualBuilder.setMetricsDataTimerangeMode('Last value'); + await visChart.waitForVisualizationRenderingStabilized(); const gaugeLabel = await visualBuilder.getGaugeLabel(); const gaugeCount = await visualBuilder.getGaugeCount(); @@ -351,6 +355,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.selectAggType('derivative', 1); await visualBuilder.setFieldForAggregation('Max of machine.ram', 1); + await visChart.waitForVisualizationRenderingStabilized(); const value = await visualBuilder.getTopNCount(); expect(value).to.eql('0'); @@ -579,6 +584,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]; await visualBuilder.clickSeriesOption(); await visualBuilder.changeDataFormatter('number'); + await visChart.waitForVisualizationRenderingStabilized(); const legendItems = await visualBuilder.getLegendItemsContent(); expect(legendItems).to.eql(expectedLegendItems); @@ -615,6 +621,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.clickTopN(); await visualBuilder.checkTopNTabIsPresent(); + await visChart.waitForVisualizationRenderingStabilized(); const topNLabel = await visualBuilder.getTopNLabel(); const topNCount = await visualBuilder.getTopNCount(); @@ -626,6 +633,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await visualBuilder.clickGauge(); await visualBuilder.checkGaugeTabIsPresent(); + await visChart.waitForVisualizationRenderingStabilized(); const gaugeLabel = await visualBuilder.getGaugeLabel(); const gaugeCount = await visualBuilder.getGaugeCount(); diff --git a/x-pack/plugins/apm/common/environment_filter_values.ts b/x-pack/plugins/apm/common/environment_filter_values.ts index ddd1ffd9b8d4..df53b08fcf56 100644 --- a/x-pack/plugins/apm/common/environment_filter_values.ts +++ b/x-pack/plugins/apm/common/environment_filter_values.ts @@ -33,13 +33,6 @@ export function getEnvironmentLabel(environment: string) { return environment; } -// #TODO Once we replace the select dropdown we can remove it -// EuiSelect > EuiSelectOption accepts text attribute -export const ENVIRONMENT_ALL_SELECT_OPTION = { - value: ENVIRONMENT_ALL_VALUE, - text: getEnvironmentLabel(ENVIRONMENT_ALL_VALUE), -}; - export const ENVIRONMENT_ALL = { value: ENVIRONMENT_ALL_VALUE, label: getEnvironmentLabel(ENVIRONMENT_ALL_VALUE), @@ -47,7 +40,7 @@ export const ENVIRONMENT_ALL = { export const ENVIRONMENT_NOT_DEFINED = { value: ENVIRONMENT_NOT_DEFINED_VALUE, - text: getEnvironmentLabel(ENVIRONMENT_NOT_DEFINED_VALUE), + label: getEnvironmentLabel(ENVIRONMENT_NOT_DEFINED_VALUE), }; export function getEnvironmentEsField(environment: string) { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts index 0dab9c6f2440..216a9c87d6db 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts @@ -20,7 +20,7 @@ const serviceInventoryHref = url.format({ query: timeRange, }); -const apiRequestsToIntercept = [ +const mainApiRequestsToIntercept = [ { endpoint: '/internal/apm/services?*', aliasName: 'servicesRequest', @@ -31,7 +31,14 @@ const apiRequestsToIntercept = [ }, ]; -const aliasNames = apiRequestsToIntercept.map( +const secondaryApiRequestsToIntercept = [ + { + endpoint: 'internal/apm/suggestions?*', + aliasName: 'suggestionsRequest', + }, +]; + +const mainAliasNames = mainApiRequestsToIntercept.map( ({ aliasName }) => `@${aliasName}` ); @@ -77,43 +84,51 @@ describe('When navigating to the service inventory', () => { describe.skip('Calls APIs', () => { beforeEach(() => { - apiRequestsToIntercept.map(({ endpoint, aliasName }) => { - cy.intercept('GET', endpoint).as(aliasName); - }); + [...mainApiRequestsToIntercept, ...secondaryApiRequestsToIntercept].map( + ({ endpoint, aliasName }) => { + cy.intercept('GET', endpoint).as(aliasName); + } + ); cy.loginAsReadOnlyUser(); cy.visit(serviceInventoryHref); }); it('with the correct environment when changing the environment', () => { - cy.wait(aliasNames); + cy.wait(mainAliasNames); + cy.get('[data-test-subj="environmentFilter"]').type('pro'); + + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: ['@suggestionsRequest'], + value: 'fieldValue=pro', + }); - cy.get('[data-test-subj="environmentFilter"]').select('production'); + cy.contains('button', 'production').click(); cy.expectAPIsToHaveBeenCalledWith({ - apisIntercepted: aliasNames, + apisIntercepted: mainAliasNames, value: 'environment=production', }); }); it('when clicking the refresh button', () => { - cy.wait(aliasNames); + cy.wait(mainAliasNames); cy.contains('Refresh').click(); - cy.wait(aliasNames); + cy.wait(mainAliasNames); }); it('when selecting a different time range and clicking the update button', () => { - cy.wait(aliasNames); + cy.wait(mainAliasNames); cy.selectAbsoluteTimeRange( moment(timeRange.rangeFrom).subtract(5, 'm').toISOString(), moment(timeRange.rangeTo).subtract(5, 'm').toISOString() ); cy.contains('Update').click(); - cy.wait(aliasNames); + cy.wait(mainAliasNames); cy.contains('Refresh').click(); - cy.wait(aliasNames); + cy.wait(mainAliasNames); }); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index 48fe14b44c79..edb1b8a82f6d 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -216,7 +216,18 @@ describe('Service Overview', () => { it('with the correct environment when changing the environment', () => { cy.wait(aliasNames, { requestTimeout: 10000 }); - cy.get('[data-test-subj="environmentFilter"]').select('production'); + cy.intercept('GET', 'internal/apm/suggestions?*').as( + 'suggestionsRequest' + ); + + cy.get('[data-test-subj="environmentFilter"]').type('pro').click(); + + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: ['@suggestionsRequest'], + value: 'fieldValue=pro', + }); + + cy.contains('button', 'production').click(); cy.expectAPIsToHaveBeenCalledWith({ apisIntercepted: aliasNames, diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index 9ea3c5aa2a6b..07373bdba9a2 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import moment from 'moment'; import { EuiFieldNumber } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -38,16 +38,21 @@ export function ServiceField({ })} > ); @@ -68,16 +73,21 @@ export function EnvironmentField({ })} > ); @@ -96,12 +106,15 @@ export function TransactionTypeField({ return ( ); diff --git a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx index 5fa3a46b0090..2fb481b26be2 100644 --- a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/form_row_suggestions_select.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import moment from 'moment'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; @@ -13,7 +13,7 @@ import { ENVIRONMENT_ALL } from '../../../../../../../common/environment_filter_ interface Props { title: string; - field: string; + fieldName: string; description: string; fieldLabel: string; value?: string; @@ -24,7 +24,7 @@ interface Props { export function FormRowSuggestionsSelect({ title, - field, + fieldName, description, fieldLabel, value, @@ -40,9 +40,9 @@ export function FormRowSuggestionsSelect({ > diff --git a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx index 1ede5cd5405c..50872375027d 100644 --- a/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/agent_configurations/agent_configuration_create_edit/service_page/service_page.tsx @@ -97,7 +97,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { 'xpack.apm.agentConfig.servicePage.service.fieldLabel', { defaultMessage: 'Service name' } )} - field={SERVICE_NAME} + fieldName={SERVICE_NAME} value={newConfig.service.name} onChange={(name) => { setNewConfig((prev) => ({ diff --git a/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/filters_section.tsx b/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/filters_section.tsx index 3b1438b4dddb..60f6003ceeeb 100644 --- a/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/filters_section.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/custom_link/create_edit_custom_link_flyout/filters_section.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import moment from 'moment'; import { EuiButtonEmpty, EuiFlexGroup, @@ -119,7 +119,7 @@ export function FiltersSection({ diff --git a/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx b/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx index 9a3d677b3f07..80217273c62d 100644 --- a/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/environment_filter/index.tsx @@ -5,18 +5,11 @@ * 2.0. */ -import { EuiSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { History } from 'history'; import React from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { - ENVIRONMENT_ALL_SELECT_OPTION, - ENVIRONMENT_NOT_DEFINED, -} from '../../../../common/environment_filter_values'; import { fromQuery, toQuery } from '../links/url_helpers'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { Environment } from '../../../../common/environment_rt'; +import { EnvironmentSelect } from '../environment_select'; import { useEnvironmentsContext } from '../../../context/environments_context/use_environments_context'; function updateEnvironmentUrl( @@ -34,77 +27,19 @@ function updateEnvironmentUrl( }); } -const SEPARATOR_OPTION = { - text: `- ${i18n.translate( - 'xpack.apm.filter.environment.selectEnvironmentLabel', - { defaultMessage: 'Select environment' } - )} -`, - disabled: true, -}; - -function getOptions(environments: string[]) { - const environmentOptions = environments - .filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value) - .map((environment) => ({ - value: environment, - text: environment, - })); - - return [ - ENVIRONMENT_ALL_SELECT_OPTION, - ...(environments.includes(ENVIRONMENT_NOT_DEFINED.value) - ? [ENVIRONMENT_NOT_DEFINED] - : []), - ...(environmentOptions.length > 0 ? [SEPARATOR_OPTION] : []), - ...environmentOptions, - ]; -} - export function ApmEnvironmentFilter() { - const { status, environments, environment } = useEnvironmentsContext(); - - return ( - - ); -} - -export function EnvironmentFilter({ - environment, - environments, - status, -}: { - environment: Environment; - environments: Environment[]; - status: FETCH_STATUS; -}) { + const { environment, environments, status } = useEnvironmentsContext(); const history = useHistory(); const location = useLocation(); - // Set the min-width so we don't see as much collapsing of the select during - // the loading state. 200px is what is looks like if "production" is - // the contents. - const minWidth = 200; - - const options = getOptions(environments); - return ( - { - updateEnvironmentUrl(history, location, event.target.value); - }} - isLoading={status === FETCH_STATUS.LOADING} - style={{ minWidth }} - data-test-subj="environmentFilter" + + updateEnvironmentUrl(history, location, changeValue) + } /> ); } diff --git a/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx b/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx new file mode 100644 index 000000000000..f954d6d69317 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/environment_select/index.tsx @@ -0,0 +1,124 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo, useState } from 'react'; +import { debounce } from 'lodash'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { + getEnvironmentLabel, + ENVIRONMENT_NOT_DEFINED, + ENVIRONMENT_ALL, +} from '../../../../common/environment_filter_values'; +import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useTimeRange } from '../../../hooks/use_time_range'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { Environment } from '../../../../common/environment_rt'; + +function getEnvironmentOptions(environments: Environment[]) { + const environmentOptions = environments + .filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value) + .map((environment) => ({ + value: environment, + label: environment, + })); + + return [ + ENVIRONMENT_ALL, + ...(environments.includes(ENVIRONMENT_NOT_DEFINED.value) + ? [ENVIRONMENT_NOT_DEFINED] + : []), + ...environmentOptions, + ]; +} + +export function EnvironmentSelect({ + environment, + availableEnvironments, + status, + onChange, +}: { + environment: Environment; + availableEnvironments: Environment[]; + status: FETCH_STATUS; + onChange: (value: string) => void; +}) { + const [searchValue, setSearchValue] = useState(''); + const { + path: { serviceName }, + query: { rangeFrom, rangeTo }, + } = useApmParams('/services/{serviceName}/*'); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + + const selectedOptions: Array> = [ + { + value: environment, + label: getEnvironmentLabel(environment), + }, + ]; + + const onSelect = (changedOptions: Array>) => { + if (changedOptions.length === 1 && changedOptions[0].value) { + onChange(changedOptions[0].value); + } + }; + + const { data, status: searchStatus } = useFetcher( + (callApmApi) => { + return isEmpty(searchValue) + ? Promise.resolve({ terms: [] }) + : callApmApi('GET /internal/apm/suggestions', { + params: { + query: { + fieldName: SERVICE_ENVIRONMENT, + fieldValue: searchValue, + serviceName, + start, + end, + }, + }, + }); + }, + [searchValue, start, end, serviceName] + ); + const terms = data?.terms ?? []; + + const options: Array> = [ + ...(searchValue === '' + ? getEnvironmentOptions(availableEnvironments) + : terms.map((name) => { + return { label: name, value: name }; + })), + ]; + + const onSearch = useMemo(() => debounce(setSearchValue, 300), []); + + return ( + onSelect(changedOptions)} + onSearchChange={onSearch} + isLoading={ + status === FETCH_STATUS.LOADING || searchStatus === FETCH_STATUS.LOADING + } + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/suggestions_select/index.tsx b/x-pack/plugins/apm/public/components/shared/suggestions_select/index.tsx index 8b8907af4bc2..66c8980bf00c 100644 --- a/x-pack/plugins/apm/public/components/shared/suggestions_select/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/suggestions_select/index.tsx @@ -11,27 +11,33 @@ import React, { useCallback, useState } from 'react'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; interface SuggestionsSelectProps { - allOption?: EuiComboBoxOptionOption; + customOptions?: Array>; customOptionText?: string; defaultValue?: string; - field: string; + fieldName: string; + start: string; + end: string; onChange: (value?: string) => void; isClearable?: boolean; isInvalid?: boolean; placeholder: string; dataTestSubj?: string; + prepend?: string; } export function SuggestionsSelect({ - allOption, + customOptions, customOptionText, defaultValue, - field, + fieldName, + start, + end, onChange, placeholder, isInvalid, dataTestSubj, isClearable = true, + prepend, }: SuggestionsSelectProps) { let defaultOption: EuiComboBoxOptionOption | undefined; @@ -48,11 +54,16 @@ export function SuggestionsSelect({ (callApmApi) => { return callApmApi('GET /internal/apm/suggestions', { params: { - query: { field, string: searchValue }, + query: { + fieldName, + fieldValue: searchValue, + start, + end, + }, }, }); }, - [field, searchValue], + [fieldName, searchValue, start, end], { preservePreviousData: false } ); @@ -85,11 +96,7 @@ export function SuggestionsSelect({ const terms = data?.terms ?? []; const options: Array> = [ - ...(allOption && - (searchValue === '' || - searchValue.toLowerCase() === allOption.label.toLowerCase()) - ? [allOption] - : []), + ...(customOptions ? customOptions : []), ...terms.map((name) => { return { label: name, value: name }; }), @@ -111,6 +118,7 @@ export function SuggestionsSelect({ style={{ minWidth: '256px' }} onCreateOption={handleCreateOption} data-test-subj={dataTestSubj} + prepend={prepend} /> ); } diff --git a/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx index 742ea46cee53..58de938a70e3 100644 --- a/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx @@ -60,11 +60,13 @@ export const Example: Story = ({ }) => { return ( {}} placeholder={placeholder} + start={'2022-04-13T10:29:28.541Z'} + end={'2021-04-13T10:29:28.541Z'} /> ); }; diff --git a/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx b/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx index 6350dd3f0a7e..41ae58ba0482 100644 --- a/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx +++ b/x-pack/plugins/apm/public/context/environments_context/environments_context.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { Environment } from '../../../common/environment_rt'; import { useApmParams } from '../../hooks/use_apm_params'; -import { useEnvironmentsFetcher } from '../../hooks/use_environments_fetcher'; import { FETCH_STATUS } from '../../hooks/use_fetcher'; import { useTimeRange } from '../../hooks/use_time_range'; +import { useEnvironmentsFetcher } from '../../hooks/use_environments_fetcher'; export const EnvironmentsContext = React.createContext<{ environment: Environment; @@ -48,9 +48,9 @@ export function EnvironmentsContextProvider({ return ( {children} diff --git a/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx index edd1b59be144..50ad012e1aa0 100644 --- a/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx @@ -5,23 +5,7 @@ * 2.0. */ -import { useMemo } from 'react'; import { useFetcher } from './use_fetcher'; -import { - ENVIRONMENT_ALL, - ENVIRONMENT_NOT_DEFINED, -} from '../../common/environment_filter_values'; - -function getEnvironmentOptions(environments: string[]) { - const environmentOptions = environments - .filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value) - .map((environment) => ({ - value: environment, - text: environment, - })); - - return [ENVIRONMENT_ALL, ...environmentOptions]; -} const INITIAL_DATA = { environments: [] }; @@ -51,10 +35,5 @@ export function useEnvironmentsFetcher({ [start, end, serviceName] ); - const environmentOptions = useMemo( - () => getEnvironmentOptions(data.environments), - [data?.environments] - ); - - return { environments: data.environments, status, environmentOptions }; + return { environments: data.environments, status }; } diff --git a/x-pack/plugins/apm/server/routes/suggestions/get_suggestions.ts b/x-pack/plugins/apm/server/routes/suggestions/get_suggestions.ts index 624bf2bb4c01..f65a843ed95d 100644 --- a/x-pack/plugins/apm/server/routes/suggestions/get_suggestions.ts +++ b/x-pack/plugins/apm/server/routes/suggestions/get_suggestions.ts @@ -4,23 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { ProcessorEvent } from '../../../common/processor_event'; import { getProcessorEventForTransactions } from '../../lib/helpers/transactions'; import { Setup } from '../../lib/helpers/setup_request'; export async function getSuggestions({ - field, + fieldName, + fieldValue, searchAggregatedTransactions, setup, size, - string, + start, + end, }: { - field: string; + fieldName: string; + fieldValue: string; searchAggregatedTransactions: boolean; setup: Setup; size: number; - string: string; + start: number; + end: number; }) { const { apmEventClient } = setup; @@ -34,9 +37,18 @@ export async function getSuggestions({ }, body: { case_insensitive: true, - field, + field: fieldName, size, - string, + string: fieldValue, + index_filter: { + range: { + ['@timestamp']: { + gte: start, + lte: end, + format: 'epoch_millis', + }, + }, + }, }, }); diff --git a/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_aggregation.ts b/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_aggregation.ts new file mode 100644 index 000000000000..268bc4151efd --- /dev/null +++ b/x-pack/plugins/apm/server/routes/suggestions/get_suggestions_with_terms_aggregation.ts @@ -0,0 +1,75 @@ +/* + * 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 { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getProcessorEventForTransactions } from '../../lib/helpers/transactions'; +import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../lib/helpers/setup_request'; + +export async function getSuggestionsWithTermsAggregation({ + fieldName, + fieldValue, + searchAggregatedTransactions, + serviceName, + setup, + size, + start, + end, +}: { + fieldName: string; + fieldValue: string; + searchAggregatedTransactions: boolean; + serviceName: string; + setup: Setup; + size: number; + start: number; + end: number; +}) { + const { apmEventClient } = setup; + + const response = await apmEventClient.search( + 'get_suggestions_with_terms_aggregation', + { + apm: { + events: [ + getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + timeout: '1500ms', + size: 0, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...rangeQuery(start, end), + { + wildcard: { + [fieldName]: `*${fieldValue}*`, + }, + }, + ], + }, + }, + aggs: { + items: { + terms: { field: fieldName, size }, + }, + }, + }, + } + ); + + return { + terms: + response.aggregations?.items.buckets.map( + (bucket) => bucket.key as string + ) ?? [], + }; +} diff --git a/x-pack/plugins/apm/server/routes/suggestions/route.ts b/x-pack/plugins/apm/server/routes/suggestions/route.ts index b49204cd86fc..68f94634a5de 100644 --- a/x-pack/plugins/apm/server/routes/suggestions/route.ts +++ b/x-pack/plugins/apm/server/routes/suggestions/route.ts @@ -8,20 +8,29 @@ import * as t from 'io-ts'; import { maxSuggestions } from '@kbn/observability-plugin/common'; import { getSuggestions } from './get_suggestions'; +import { getSuggestionsWithTermsAggregation } from './get_suggestions_with_terms_aggregation'; import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions'; import { setupRequest } from '../../lib/helpers/setup_request'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { rangeRt } from '../default_api_types'; const suggestionsRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/suggestions', - params: t.partial({ - query: t.type({ field: t.string, string: t.string }), + params: t.type({ + query: t.intersection([ + t.type({ + fieldName: t.string, + fieldValue: t.string, + }), + rangeRt, + t.partial({ serviceName: t.string }), + ]), }), options: { tags: ['access:apm'] }, handler: async (resources): Promise<{ terms: string[] }> => { const setup = await setupRequest(resources); const { context, params } = resources; - const { field, string } = params.query; + const { fieldName, fieldValue, serviceName, start, end } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions({ apmEventClient: setup.apmEventClient, config: setup.config, @@ -31,16 +40,32 @@ const suggestionsRoute = createApmServerRoute({ const size = await coreContext.uiSettings.client.get( maxSuggestions ); - const suggestions = await getSuggestions({ - field, - searchAggregatedTransactions, - setup, - size, - string, - }); + + const suggestions = serviceName + ? await getSuggestionsWithTermsAggregation({ + fieldName, + fieldValue, + searchAggregatedTransactions, + serviceName, + setup, + size, + start, + end, + }) + : await getSuggestions({ + fieldName, + fieldValue, + searchAggregatedTransactions, + setup, + size, + start, + end, + }); return suggestions; }, }); -export const suggestionsRouteRepository = suggestionsRoute; +export const suggestionsRouteRepository = { + ...suggestionsRoute, +}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 8add1aa41b83..dc164f862aa5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React from 'react'; +import React, { FC } from 'react'; +import useObservable from 'react-use/lib/useObservable'; import ReactDOM from 'react-dom'; import { CoreStart } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; @@ -32,9 +33,28 @@ const embeddablesRegistry: { const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { const I18nContext = core.i18n.Context; + const EmbeddableRenderer: FC<{ embeddable: IEmbeddable }> = ({ embeddable }) => { + const currentAppId = useObservable(core.application.currentAppId$, undefined); - const embeddableContainerContext: EmbeddableContainerContext = { - getCurrentPath: () => window.location.hash, + if (!currentAppId) { + return null; + } + + const embeddableContainerContext: EmbeddableContainerContext = { + getCurrentPath: () => { + const urlToApp = core.application.getUrlForApp(currentAppId); + const inAppPath = window.location.pathname.replace(urlToApp, ''); + + return inAppPath + window.location.search + window.location.hash; + }, + }; + + return ( + + ); }; return (embeddableObject: IEmbeddable) => { @@ -45,10 +65,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { > - + diff --git a/x-pack/plugins/canvas/public/components/app/index.tsx b/x-pack/plugins/canvas/public/components/app/index.tsx index d9a8b5fa5469..82404f651ec3 100644 --- a/x-pack/plugins/canvas/public/components/app/index.tsx +++ b/x-pack/plugins/canvas/public/components/app/index.tsx @@ -5,14 +5,9 @@ * 2.0. */ -import React, { FC, useRef, useEffect } from 'react'; -import { Observable } from 'rxjs'; +import React, { FC, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { History } from 'history'; -// @ts-expect-error -import createHashStateHistory from 'history-extra/dist/createHashStateHistory'; import { ScopedHistory } from '@kbn/core/public'; -import { skipWhile, timeout, take } from 'rxjs/operators'; import { useNavLinkService } from '../../services'; // @ts-expect-error import { shortcutManager } from '../../lib/shortcut_manager'; @@ -33,64 +28,18 @@ class ShortcutManagerContextWrapper extends React.Component { } export const App: FC<{ history: ScopedHistory }> = ({ history }) => { - const historyRef = useRef(createHashStateHistory() as History); const { updatePath } = useNavLinkService(); useEffect(() => { - return historyRef.current.listen(({ pathname }) => { - updatePath(pathname); + return history.listen(({ pathname, search }) => { + updatePath(pathname + search); }); }); - useEffect(() => { - return history.listen(({ pathname, hash }) => { - // The scoped history could have something that triggers a url change, and that change is not seen by - // our hash router. For example, a scopedHistory.replace() as done as part of the saved object resolve - // alias match flow will do the replace on the scopedHistory, and our app doesn't react appropriately - - // So, to work around this, whenever we see a url on the scoped history, we're going to wait a beat and see - // if it shows up in our hash router. If it doesn't, then we're going to force it onto our hash router - - // I don't like this at all, and to overcome this we should switch away from hash router sooner rather than later - // and just use scopedHistory as our history object - const expectedPath = hash.substr(1); - const action = history.action; - - // Observable of all the path - const hashPaths$ = new Observable((subscriber) => { - subscriber.next(historyRef.current.location.pathname); - - const unsubscribeHashListener = historyRef.current.listen(({ pathname: newPath }) => { - subscriber.next(newPath); - }); - - return unsubscribeHashListener; - }); - - const subscription = hashPaths$ - .pipe( - skipWhile((value) => value !== expectedPath), - timeout(100), - take(1) - ) - .subscribe({ - error: (e) => { - if (action === 'REPLACE') { - historyRef.current.replace(expectedPath); - } else { - historyRef.current.push(expectedPath); - } - }, - }); - - window.setTimeout(() => subscription.unsubscribe(), 150); - }); - }, [history, historyRef]); - return (
- +
); diff --git a/x-pack/plugins/canvas/public/components/home/home.tsx b/x-pack/plugins/canvas/public/components/home/home.tsx index 6b356ada8681..98dc99d6caaa 100644 --- a/x-pack/plugins/canvas/public/components/home/home.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.tsx @@ -7,14 +7,10 @@ import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; - -import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; import { resetWorkpad } from '../../state/actions/workpad'; import { Home as Component } from './home.component'; -import { usePlatformService } from '../../services'; export const Home = () => { - const { setBreadcrumbs } = usePlatformService(); const [isMounted, setIsMounted] = useState(false); const dispatch = useDispatch(); @@ -25,9 +21,5 @@ export const Home = () => { } }, [dispatch, isMounted, setIsMounted]); - useEffect(() => { - setBreadcrumbs([getBaseBreadcrumb()]); - }, [setBreadcrumbs]); - return ; }; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/workpad_table.stories.storyshot b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/workpad_table.stories.storyshot index 7536cc7acf7d..d2e52dde1ad5 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/workpad_table.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/workpad_table.stories.storyshot @@ -382,6 +382,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiLink euiLink--primary" data-test-subj="canvasWorkpadTableWorkpad" href="/workpad/workpad-2" + onClick={[Function]} rel="noreferrer" > @@ -558,6 +559,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiLink euiLink--primary" data-test-subj="canvasWorkpadTableWorkpad" href="/workpad/workpad-1" + onClick={[Function]} rel="noreferrer" > @@ -734,6 +736,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiLink euiLink--primary" data-test-subj="canvasWorkpadTableWorkpad" href="/workpad/workpad-0" + onClick={[Function]} rel="noreferrer" > diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx index b288612450bf..086a737d525e 100644 --- a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx +++ b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; import { resetWorkpad } from '../../state/actions/workpad'; @@ -16,10 +17,11 @@ export const HomeApp = () => { const { setBreadcrumbs } = usePlatformService(); const dispatch = useDispatch(); const onLoad = () => dispatch(resetWorkpad()); + const history = useHistory(); useEffect(() => { - setBreadcrumbs([getBaseBreadcrumb()]); - }, [setBreadcrumbs]); + setBreadcrumbs([getBaseBreadcrumb(history)]); + }, [setBreadcrumbs, history]); return ; }; diff --git a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx index bb3123de3fec..5f82b5050a17 100644 --- a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx +++ b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, MouseEvent } from 'react'; import { EuiLink, EuiLinkProps, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; @@ -15,13 +15,43 @@ interface RoutingProps { type RoutingLinkProps = Omit & RoutingProps; +const isModifiedEvent = (event: MouseEvent) => + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); + +const isLeftClickEvent = (event: MouseEvent) => event.button === 0; + +const isTargetBlank = (event: MouseEvent) => { + const target = (event.target as HTMLElement).getAttribute('target'); + return target && target !== '_self'; +}; + export const RoutingLink: FC = ({ to, ...rest }) => { const history = useHistory(); + const onClick = useCallback( + (event: MouseEvent) => { + if (event.defaultPrevented) { + return; + } + + // Let the browser handle links that open new tabs/windows + if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) { + return; + } + + // Prevent regular link behavior, which causes a browser refresh. + event.preventDefault(); + + // Push the route to the history. + history.push(to); + }, + [history, to] + ); + // Generate the correct link href (with basename accounted for) const href = history.createHref({ pathname: to }); - const props = { ...rest, href } as EuiLinkProps; + const props = { ...rest, href, onClick } as EuiLinkProps; return ; }; @@ -31,10 +61,30 @@ type RoutingButtonIconProps = Omit & Rou export const RoutingButtonIcon: FC = ({ to, ...rest }) => { const history = useHistory(); + const onClick = useCallback( + (event: MouseEvent) => { + if (event.defaultPrevented) { + return; + } + + // Let the browser handle links that open new tabs/windows + if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) { + return; + } + + // Prevent regular link behavior, which causes a browser refresh. + event.preventDefault(); + + // Push the route to the history. + history.push(to); + }, + [history, to] + ); + // Generate the correct link href (with basename accounted for) const href = history.createHref({ pathname: to }); - const props = { ...rest, href } as EuiButtonIconProps; + const props = { ...rest, href, onClick } as EuiButtonIconProps; return ; }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx index c02ac90f8066..8c10efe1b10d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -29,7 +29,7 @@ interface Props { export const EditorMenu: FC = ({ addElement }) => { const embeddablesService = useEmbeddablesService(); - const { pathname, search } = useLocation(); + const { pathname, search, hash } = useLocation(); const platformService = usePlatformService(); const stateTransferService = embeddablesService.getStateTransfer(); const visualizationsService = useVisualizationsService(); @@ -61,11 +61,11 @@ export const EditorMenu: FC = ({ addElement }) => { path, state: { originatingApp: CANVAS_APP, - originatingPath: `#/${pathname}${search}`, + originatingPath: `${pathname}${search}${hash}`, }, }); }, - [stateTransferService, pathname, search] + [stateTransferService, pathname, search, hash] ); const createNewEmbeddable = useCallback( diff --git a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts index 02036be10322..53f0fbbfa334 100644 --- a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts +++ b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts @@ -5,12 +5,47 @@ * 2.0. */ +import { MouseEvent } from 'react'; +import { History } from 'history'; import { ChromeBreadcrumb } from '@kbn/core/public'; -export const getBaseBreadcrumb = (): ChromeBreadcrumb => ({ - text: 'Canvas', - href: '#/', -}); +const isModifiedEvent = (event: MouseEvent) => + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); + +const isLeftClickEvent = (event: MouseEvent) => event.button === 0; + +const isTargetBlank = (event: MouseEvent) => { + const target = (event.target as HTMLElement).getAttribute('target'); + return target && target !== '_self'; +}; + +export const getBaseBreadcrumb = (history: History): ChromeBreadcrumb => { + const path = '/'; + const href = history.createHref({ pathname: path }); + + const onClick = (event: MouseEvent) => { + if (event.defaultPrevented) { + return; + } + + // Let the browser handle links that open new tabs/windows + if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) { + return; + } + + // Prevent regular link behavior, which causes a browser refresh. + event.preventDefault(); + + // Push the route to the history. + history.push(path); + }; + + return { + text: 'Canvas', + href, + onClick, + }; +}; export const getWorkpadBreadcrumb = ({ name = 'Workpad', diff --git a/x-pack/plugins/canvas/public/routes/index.tsx b/x-pack/plugins/canvas/public/routes/index.tsx index fd09aeae3fa9..e7e9cd6541a3 100644 --- a/x-pack/plugins/canvas/public/routes/index.tsx +++ b/x-pack/plugins/canvas/public/routes/index.tsx @@ -6,17 +6,48 @@ */ import React, { FC } from 'react'; -import { Router, Switch } from 'react-router-dom'; +import { Router, Switch, Route, RouteComponentProps, Redirect } from 'react-router-dom'; import { History } from 'history'; +import { parse, stringify } from 'query-string'; import { HomeRoute } from './home'; import { WorkpadRoute, ExportWorkpadRoute } from './workpad'; +const isHashPath = (hash: string) => { + return hash.indexOf('#/') === 0; +}; + +const mergeQueryStrings = (query: string, queryFromHash: string) => { + const queryObject = parse(query); + const hashObject = parse(queryFromHash); + + return stringify({ ...queryObject, ...hashObject }); +}; + export const CanvasRouter: FC<{ history: History }> = ({ history }) => ( - - {ExportWorkpadRoute()} - {WorkpadRoute()} - {HomeRoute()} - + { + // If it looks like the hash is a route then we will do a redirect + if (isHashPath(route.location.hash)) { + const [hashPath, hashQuery] = route.location.hash.split('?'); + let search = route.location.search || '?'; + + if (hashQuery !== undefined) { + search = mergeQueryStrings(search, `?${hashQuery}`); + } + + return 1 ? `?${search}` : ''}`} />; + } + + return ( + + {ExportWorkpadRoute()} + {WorkpadRoute()} + {HomeRoute()} + + ); + }} + /> ); diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx index 084c9d8c76b0..d6c4b1c6277f 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { FC, useEffect } from 'react'; import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { getBaseBreadcrumb, getWorkpadBreadcrumb } from '../../lib/breadcrumbs'; // @ts-expect-error import { setDocTitle } from '../../lib/doc_title'; @@ -27,13 +28,14 @@ export const WorkpadPresentationHelper: FC = ({ children }) => { useFullscreenPresentationHelper(); useAutoplayHelper(); useRefreshHelper(); + const history = useHistory(); useEffect(() => { platformService.setBreadcrumbs([ - getBaseBreadcrumb(), + getBaseBreadcrumb(history), getWorkpadBreadcrumb({ name: workpad.name }), ]); - }, [workpad.name, workpad.id, platformService]); + }, [workpad.name, workpad.id, platformService, history]); useEffect(() => { setDocTitle(workpad.name); @@ -44,7 +46,7 @@ export const WorkpadPresentationHelper: FC = ({ children }) => { objectNoun: getWorkpadLabel(), currentObjectId: workpad.id, otherObjectId: workpad.aliasId, - otherObjectPath: `#/workpad/${workpad.aliasId}`, + otherObjectPath: `/workpad/${workpad.aliasId}`, }) : null; diff --git a/x-pack/plugins/canvas/public/services/kibana/nav_link.ts b/x-pack/plugins/canvas/public/services/kibana/nav_link.ts index cf68b69155ad..8470c688f5b2 100644 --- a/x-pack/plugins/canvas/public/services/kibana/nav_link.ts +++ b/x-pack/plugins/canvas/public/services/kibana/nav_link.ts @@ -20,7 +20,7 @@ export type CanvasNavLinkServiceFactory = KibanaPluginServiceFactory< export const navLinkServiceFactory: CanvasNavLinkServiceFactory = ({ coreStart, appUpdater }) => ({ updatePath: (path: string) => { appUpdater?.next(() => ({ - defaultPath: `#${path}`, + defaultPath: `${path}`, })); getSessionStorage().set(`${SESSIONSTORAGE_LASTPATH}:${coreStart.http.basePath.get()}`, path); diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index a64a2604609f..a351e6f271c7 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -13,7 +13,6 @@ export const UPDATE_RULES_CONFIG_ROUTE_PATH = export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture'; -export const AGENT_LOGS_INDEX_PATTERN = '.logs-cloud_security_posture.metadata*'; export const CSP_LATEST_FINDINGS_DATA_VIEW = 'logs-cloud_security_posture.findings_latest-*'; export const FINDINGS_INDEX_PATTERN = 'logs-cloud_security_posture.findings-default*'; diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts index 32d53b0a8969..4ee09119c15e 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts @@ -41,7 +41,7 @@ export const initializeCspTransformsIndices = async ( export const createIndexIfNotExists = async ( esClient: ElasticsearchClient, - indexName: string, + indexTemplateName: string, indexPattern: string, mappings: MappingTypeMapping, logger: Logger @@ -53,7 +53,7 @@ export const createIndexIfNotExists = async ( if (!isLatestIndexExists) { await esClient.indices.putIndexTemplate({ - name: indexName, + name: indexTemplateName, index_patterns: indexPattern, template: { mappings }, priority: 500, @@ -65,7 +65,7 @@ export const createIndexIfNotExists = async ( } } catch (err) { const error = transformError(err); - logger.error(`Failed to create ${LATEST_FINDINGS_INDEX_DEFAULT_NS}`); + logger.error(`Failed to create the index template: ${indexTemplateName}`); logger.error(error.message); } }; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts index ae43443f1571..e9b9401d1373 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts @@ -37,8 +37,9 @@ export const getPackagePolicy = async ( throw new Error(`package policy Id '${packagePolicyId}' is not exist`); } if (packagePolicies[0].package?.name !== CLOUD_SECURITY_POSTURE_PACKAGE_NAME) { - // TODO: improve this validator to support any future CSP package - throw new Error(`Package Policy Id '${packagePolicyId}' is not CSP package`); + throw new Error( + `Package Policy Id '${packagePolicyId}' is not of type cloud security posture package` + ); } return packagePolicies![0]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 19263f057e40..984e6664681b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -30,8 +30,6 @@ export interface Group { createdAt: string; updatedAt: string; contentSources: ContentSource[]; - users: User[]; - usersCount: number; color?: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts index 0e072210d248..02e80d9f8c5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts @@ -6,12 +6,11 @@ */ import { DEFAULT_META } from '../../../../shared/constants'; -import { ContentSource, User, Group } from '../../../types'; +import { ContentSource, Group } from '../../../types'; export const mockGroupsValues = { groups: [] as Group[], contentSources: [] as ContentSource[], - users: [] as User[], groupsDataLoading: true, groupListLoading: true, newGroupModalOpen: false, @@ -21,10 +20,6 @@ export const mockGroupsValues = { newGroupNameErrors: [], filterSourcesDropdownOpen: false, filteredSources: [], - filterUsersDropdownOpen: false, - filteredUsers: [], - allGroupUsersLoading: false, - allGroupUsers: [], filterValue: '', groupsMeta: DEFAULT_META, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx index e1ecc47f0266..97b2879ceef5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx @@ -91,7 +91,6 @@ describe('GroupOverview', () => { ...mockValues, group: { ...groups[0], - users: [], contentSources: [], }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index effacfa3aa4f..d118037a2d80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -28,12 +28,6 @@ export const NO_SOURCES_MESSAGE = i18n.translate( defaultMessage: 'No organizational content sources', } ); -export const NO_USERS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage', - { - defaultMessage: 'No users', - } -); const dateDisplay = (date: string) => moment(date).isAfter(moment().subtract(DAYS_CUTOFF, 'days')) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 7af93490b2eb..5b8b01a4bb1e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -38,7 +38,6 @@ export const Groups: React.FC = () => { page: { total_results: numGroups }, }, filteredSources, - filteredUsers, filterValue, } = useValues(GroupsLogic); @@ -47,7 +46,7 @@ export const Groups: React.FC = () => { useEffect(() => { getSearchResults(true); return resetGroups; - }, [filteredSources, filteredUsers, filterValue]); + }, [filteredSources, filterValue]); if (newGroup && hasMessages) { messages[0].description = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts index 97163f152993..bc82c9587167 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -12,7 +12,6 @@ import { } from '../../../__mocks__/kea_logic'; import { contentSources } from '../../__mocks__/content_sources.mock'; import { groups } from '../../__mocks__/groups.mock'; -import { users } from '../../__mocks__/users.mock'; import { mockGroupsValues } from './__mocks__/groups_logic.mock'; import { nextTick } from '@kbn/test-jest-helpers'; @@ -49,13 +48,12 @@ describe('GroupsLogic', () => { describe('actions', () => { describe('onInitializeGroups', () => { it('sets reducers', () => { - GroupsLogic.actions.onInitializeGroups({ contentSources, users }); + GroupsLogic.actions.onInitializeGroups({ contentSources }); expect(GroupsLogic.values).toEqual({ ...mockGroupsValues, groupsDataLoading: false, contentSources, - users, }); }); }); @@ -103,59 +101,6 @@ describe('GroupsLogic', () => { }); }); - describe('addFilteredUser', () => { - it('sets reducers', () => { - GroupsLogic.actions.addFilteredUser('foo'); - GroupsLogic.actions.addFilteredUser('bar'); - GroupsLogic.actions.addFilteredUser('baz'); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - hasFiltersSet: true, - filteredUsers: ['bar', 'baz', 'foo'], - }); - }); - }); - - describe('removeFilteredUser', () => { - it('sets reducers', () => { - GroupsLogic.actions.addFilteredUser('foo'); - GroupsLogic.actions.addFilteredUser('bar'); - GroupsLogic.actions.addFilteredUser('baz'); - GroupsLogic.actions.removeFilteredUser('foo'); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - hasFiltersSet: true, - filteredUsers: ['bar', 'baz'], - }); - }); - }); - - describe('setGroupUsers', () => { - it('sets reducers', () => { - GroupsLogic.actions.setGroupUsers(users); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - allGroupUsersLoading: false, - allGroupUsers: users, - }); - }); - }); - - describe('setAllGroupLoading', () => { - it('sets reducer', () => { - GroupsLogic.actions.setAllGroupLoading(true); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - allGroupUsersLoading: true, - allGroupUsers: [], - }); - }); - }); - describe('setFilterValue', () => { it('sets reducer', () => { GroupsLogic.actions.setFilterValue('foo'); @@ -190,7 +135,6 @@ describe('GroupsLogic', () => { newGroup: groups[0], newGroupNameErrors: [], filteredSources: [], - filteredUsers: [], groupsMeta: DEFAULT_META, }); }); @@ -234,19 +178,6 @@ describe('GroupsLogic', () => { }); }); - describe('closeFilterUsersDropdown', () => { - it('sets reducer', () => { - // Open dropdown first - GroupsLogic.actions.toggleFilterUsersDropdown(); - GroupsLogic.actions.closeFilterUsersDropdown(); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - filterUsersDropdownOpen: false, - }); - }); - }); - describe('setGroupsLoading', () => { it('sets reducer', () => { // Set to false first @@ -294,7 +225,6 @@ describe('GroupsLogic', () => { const search = { query: '', content_source_ids: [], - user_ids: [], }; const payload = { @@ -352,22 +282,6 @@ describe('GroupsLogic', () => { }); }); - describe('fetchGroupUsers', () => { - it('calls API and sets values', async () => { - const setGroupUsersSpy = jest.spyOn(GroupsLogic.actions, 'setGroupUsers'); - http.get.mockReturnValue(Promise.resolve(users)); - - GroupsLogic.actions.fetchGroupUsers('123'); - expect(http.get).toHaveBeenCalledWith('/internal/workplace_search/groups/123/group_users'); - await nextTick(); - expect(setGroupUsersSpy).toHaveBeenCalledWith(users); - }); - - itShowsServerErrorAsFlashMessage(http.get, () => { - GroupsLogic.actions.fetchGroupUsers('123'); - }); - }); - describe('saveNewGroup', () => { it('calls API and sets values', async () => { const GROUP_NAME = 'new group'; @@ -430,7 +344,6 @@ describe('GroupsLogic', () => { expect(GroupsLogic.values).toEqual({ ...mockGroupsValues, filteredSources: [], - filteredUsers: [], filterValue: '', groupsMeta: DEFAULT_META, }); @@ -449,17 +362,5 @@ describe('GroupsLogic', () => { expect(clearFlashMessages).toHaveBeenCalled(); }); }); - - describe('toggleFilterUsersDropdown', () => { - it('sets reducer and clears flash messages', () => { - GroupsLogic.actions.toggleFilterUsersDropdown(); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - filterUsersDropdownOpen: true, - }); - expect(clearFlashMessages).toHaveBeenCalled(); - }); - }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts index c14538346ad3..3e137ea8a671 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts @@ -18,13 +18,12 @@ import { flashSuccessToast, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; -import { ContentSource, Group, User } from '../../types'; +import { ContentSource, Group } from '../../types'; export const MAX_NAME_LENGTH = 40; interface GroupsServerData { contentSources: ContentSource[]; - users: User[]; } interface GroupsSearchResponse { @@ -37,10 +36,6 @@ interface GroupsActions { setSearchResults(data: GroupsSearchResponse): GroupsSearchResponse; addFilteredSource(sourceId: string): string; removeFilteredSource(sourceId: string): string; - addFilteredUser(userId: string): string; - removeFilteredUser(userId: string): string; - setGroupUsers(allGroupUsers: User[]): User[]; - setAllGroupLoading(allGroupUsersLoading: boolean): boolean; setFilterValue(filterValue: string): string; setActivePage(activePage: number): number; setNewGroupName(newGroupName: string): string; @@ -49,22 +44,18 @@ interface GroupsActions { openNewGroupModal(): void; closeNewGroupModal(): void; closeFilterSourcesDropdown(): void; - closeFilterUsersDropdown(): void; toggleFilterSourcesDropdown(): void; - toggleFilterUsersDropdown(): void; setGroupsLoading(): void; resetGroupsFilters(): void; resetGroups(): void; initializeGroups(): void; getSearchResults(resetPagination?: boolean): { resetPagination: boolean | undefined }; - fetchGroupUsers(groupId: string): { groupId: string }; saveNewGroup(): void; } interface GroupsValues { groups: Group[]; contentSources: ContentSource[]; - users: User[]; groupsDataLoading: boolean; groupListLoading: boolean; newGroupModalOpen: boolean; @@ -73,10 +64,6 @@ interface GroupsValues { newGroupNameErrors: string[]; filterSourcesDropdownOpen: boolean; filteredSources: string[]; - filterUsersDropdownOpen: boolean; - filteredUsers: string[]; - allGroupUsersLoading: boolean; - allGroupUsers: User[]; filterValue: string; groupsMeta: Meta; hasFiltersSet: boolean; @@ -89,10 +76,6 @@ export const GroupsLogic = kea>({ setSearchResults: (data) => data, addFilteredSource: (sourceId) => sourceId, removeFilteredSource: (sourceId) => sourceId, - addFilteredUser: (userId) => userId, - removeFilteredUser: (userId) => userId, - setGroupUsers: (allGroupUsers) => allGroupUsers, - setAllGroupLoading: (allGroupUsersLoading: boolean) => allGroupUsersLoading, setFilterValue: (filterValue) => filterValue, setActivePage: (activePage) => activePage, setNewGroupName: (newGroupName) => newGroupName, @@ -101,15 +84,12 @@ export const GroupsLogic = kea>({ openNewGroupModal: () => true, closeNewGroupModal: () => true, closeFilterSourcesDropdown: () => true, - closeFilterUsersDropdown: () => true, toggleFilterSourcesDropdown: () => true, - toggleFilterUsersDropdown: () => true, setGroupsLoading: () => true, resetGroupsFilters: () => true, resetGroups: () => true, initializeGroups: () => true, getSearchResults: (resetPagination) => ({ resetPagination }), - fetchGroupUsers: (groupId) => ({ groupId }), saveNewGroup: () => true, }, reducers: { @@ -125,12 +105,6 @@ export const GroupsLogic = kea>({ onInitializeGroups: (_, { contentSources }) => contentSources, }, ], - users: [ - [], - { - onInitializeGroups: (_, { users }) => users, - }, - ], groupsDataLoading: [ true, { @@ -193,36 +167,6 @@ export const GroupsLogic = kea>({ removeFilteredSource: (state, sourceId) => state.filter((id) => id !== sourceId), }, ], - filterUsersDropdownOpen: [ - false, - { - toggleFilterUsersDropdown: (state) => !state, - closeFilterUsersDropdown: () => false, - }, - ], - filteredUsers: [ - [], - { - resetGroupsFilters: () => [], - setNewGroup: () => [], - addFilteredUser: (state, userId) => [...state, userId].sort(), - removeFilteredUser: (state, userId) => state.filter((id) => id !== userId), - }, - ], - allGroupUsersLoading: [ - false, - { - setAllGroupLoading: (_, allGroupUsersLoading) => allGroupUsersLoading, - setGroupUsers: () => false, - }, - ], - allGroupUsers: [ - [], - { - setGroupUsers: (_, allGroupUsers) => allGroupUsers, - setAllGroupLoading: () => [], - }, - ], filterValue: [ '', { @@ -248,8 +192,8 @@ export const GroupsLogic = kea>({ }, selectors: ({ selectors }) => ({ hasFiltersSet: [ - () => [selectors.filteredUsers, selectors.filteredSources], - (filteredUsers, filteredSources) => filteredUsers.length > 0 || filteredSources.length > 0, + () => [selectors.filteredSources], + (filteredSources) => filteredSources.length > 0, ], }), listeners: ({ actions, values }) => ({ @@ -275,7 +219,6 @@ export const GroupsLogic = kea>({ }, filterValue, filteredSources, - filteredUsers, } = values; // Is the user changes the query while on a different page, we want to start back over at 1. @@ -286,7 +229,6 @@ export const GroupsLogic = kea>({ const search = { query: filterValue, content_source_ids: filteredSources, - user_ids: filteredUsers, }; try { @@ -306,17 +248,6 @@ export const GroupsLogic = kea>({ flashAPIErrors(e); } }, - fetchGroupUsers: async ({ groupId }) => { - actions.setAllGroupLoading(true); - try { - const response = await HttpLogic.values.http.get( - `/internal/workplace_search/groups/${groupId}/group_users` - ); - actions.setGroupUsers(response); - } catch (e) { - flashAPIErrors(e); - } - }, saveNewGroup: async () => { try { const response = await HttpLogic.values.http.post( @@ -354,8 +285,5 @@ export const GroupsLogic = kea>({ toggleFilterSourcesDropdown: () => { clearFlashMessages(); }, - toggleFilterUsersDropdown: () => { - clearFlashMessages(); - }, }), }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts index 40ee46c7a9ff..dc1308a4140d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts @@ -107,7 +107,6 @@ describe('groups routes', () => { search: { query: 'foo', content_source_ids: ['123', '234'], - user_ids: ['345', '456'], }, }, }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts index c5c161cf7b2f..8dc153e7a292 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts @@ -51,7 +51,6 @@ export function registerSearchGroupsRoute({ search: schema.object({ query: schema.string(), content_source_ids: schema.arrayOf(schema.string()), - user_ids: schema.arrayOf(schema.string()), }), }), }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agents_selection_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agents_selection_status.tsx new file mode 100644 index 000000000000..57728f275ccb --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agents_selection_status.tsx @@ -0,0 +1,134 @@ +/* + * 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 styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; + +import { SO_SEARCH_LIMIT } from '../../../../constants'; +import type { Agent } from '../../../../types'; + +import type { SelectionMode } from './types'; + +const Divider = styled.div` + width: 0; + height: ${(props) => props.theme.eui.euiSizeL}; + border-left: ${(props) => props.theme.eui.euiBorderThin}; +`; + +const FlexItem = styled(EuiFlexItem)` + height: ${(props) => props.theme.eui.euiSizeL}; +`; + +const Button = styled(EuiButtonEmpty)` + .euiButtonEmpty__text { + font-size: ${(props) => props.theme.eui.euiFontSizeXS}; + } +`; + +export const AgentsSelectionStatus: React.FunctionComponent<{ + totalAgents: number; + selectableAgents: number; + selectionMode: SelectionMode; + setSelectionMode: (mode: SelectionMode) => void; + selectedAgents: Agent[]; + setSelectedAgents: (agents: Agent[]) => void; +}> = ({ + totalAgents, + selectableAgents, + selectionMode, + setSelectionMode, + selectedAgents, + setSelectedAgents, +}) => { + const showSelectEverything = + selectionMode === 'manual' && + selectedAgents.length === selectableAgents && + selectableAgents < totalAgents; + + return ( + <> + + + + {totalAgents > SO_SEARCH_LIMIT ? ( + , + total: , + }} + /> + ) : ( + + )} + + + {(selectionMode === 'manual' && selectedAgents.length) || + (selectionMode === 'query' && totalAgents > 0) ? ( + <> + + + + + + + + + {showSelectEverything ? ( + <> + + + + + + + + ) : null} + + + + + + + + ) : ( + + )} + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx new file mode 100644 index 000000000000..9ddf8608fc01 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { ThemeProvider } from 'styled-components'; + +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +import { coreMock } from '@kbn/core/public/mocks'; +import { registerTestBed } from '@kbn/test-jest-helpers'; + +import type { Agent } from '../../../../types'; + +import { FleetStatusProvider, ConfigContext, KibanaVersionContext } from '../../../../../../hooks'; + +import { getMockTheme } from '../../../../../../mocks'; + +import { AgentBulkActions } from './bulk_actions'; +import type { Props } from './bulk_actions'; + +const mockTheme = getMockTheme({ + eui: { + euiSize: '10px', + }, +}); + +const TestComponent = (props: Props) => ( + + + + + + + + + + + +); + +describe('AgentBulkActions', () => { + it('should show no Actions button when no agent is selected', async () => { + const selectedAgents: Agent[] = []; + const props: Props = { + totalAgents: 10, + totalInactiveAgents: 2, + selectionMode: 'manual', + currentQuery: '', + selectedAgents, + refreshAgents: () => undefined, + }; + const testBed = registerTestBed(TestComponent)(props); + const { exists } = testBed; + + expect(exists('agentBulkActionsButton')).toBe(false); + }); + + it('should show an Actions button when at least an agent is selected', async () => { + const selectedAgents: Agent[] = [ + { + id: 'Agent1', + status: 'online', + packages: ['system'], + type: 'PERMANENT', + active: true, + enrolled_at: `${Date.now()}`, + user_provided_metadata: {}, + local_metadata: {}, + }, + ]; + const props: Props = { + totalAgents: 10, + totalInactiveAgents: 2, + selectionMode: 'manual', + currentQuery: '', + selectedAgents, + refreshAgents: () => undefined, + }; + const testBed = registerTestBed(TestComponent)(props); + const { exists } = testBed; + + expect(exists('agentBulkActionsButton')).not.toBeNull(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index 6fd34f023999..a2515b51814e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -10,16 +10,14 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, - EuiText, EuiPopover, EuiContextMenu, - EuiButtonEmpty, + EuiButton, EuiIcon, EuiPortal, } from '@elastic/eui'; -import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; +import { FormattedMessage } from '@kbn/i18n-react'; -import { SO_SEARCH_LIMIT } from '../../../../constants'; import type { Agent } from '../../../../types'; import { AgentReassignAgentPolicyModal, @@ -28,43 +26,26 @@ import { } from '../../components'; import { useKibanaVersion } from '../../../../hooks'; -const Divider = styled.div` - width: 0; - height: ${(props) => props.theme.eui.euiSizeL}; - border-left: ${(props) => props.theme.eui.euiBorderThin}; -`; +import type { SelectionMode } from './types'; const FlexItem = styled(EuiFlexItem)` height: ${(props) => props.theme.eui.euiSizeL}; `; - -const Button = styled(EuiButtonEmpty)` - .euiButtonEmpty__text { - font-size: ${(props) => props.theme.eui.euiFontSizeXS}; - } -`; - -export type SelectionMode = 'manual' | 'query'; - -export const AgentBulkActions: React.FunctionComponent<{ +export interface Props { totalAgents: number; totalInactiveAgents: number; - selectableAgents: number; selectionMode: SelectionMode; - setSelectionMode: (mode: SelectionMode) => void; currentQuery: string; selectedAgents: Agent[]; - setSelectedAgents: (agents: Agent[]) => void; refreshAgents: () => void; -}> = ({ +} + +export const AgentBulkActions: React.FunctionComponent = ({ totalAgents, totalInactiveAgents, - selectableAgents, selectionMode, - setSelectionMode, currentQuery, selectedAgents, - setSelectedAgents, refreshAgents, }) => { const kibanaVersion = useKibanaVersion(); @@ -92,6 +73,7 @@ export const AgentBulkActions: React.FunctionComponent<{ name: ( ), @@ -106,6 +88,7 @@ export const AgentBulkActions: React.FunctionComponent<{ name: ( ), @@ -120,6 +103,7 @@ export const AgentBulkActions: React.FunctionComponent<{ name: ( ), @@ -130,29 +114,10 @@ export const AgentBulkActions: React.FunctionComponent<{ setIsUpgradeModalOpen(true); }, }, - { - name: ( - - ), - icon: , - onClick: () => { - closeMenu(); - setSelectionMode('manual'); - setSelectedAgents([]); - }, - }, ], }, ]; - const showSelectEverything = - selectionMode === 'manual' && - selectedAgents.length === selectableAgents && - selectableAgents < totalAgents; - const totalActiveAgents = totalAgents - totalInactiveAgents; const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents; const agents = selectionMode === 'manual' ? selectedAgents : currentQuery; @@ -196,51 +161,25 @@ export const AgentBulkActions: React.FunctionComponent<{ )} - - - {totalAgents > SO_SEARCH_LIMIT ? ( - , - total: , - }} - /> - ) : ( - - )} - - {(selectionMode === 'manual' && selectedAgents.length) || (selectionMode === 'query' && totalAgents > 0) ? ( <> - - - - + } isOpen={isMenuOpen} closePopover={closeMenu} @@ -250,22 +189,6 @@ export const AgentBulkActions: React.FunctionComponent<{ - {showSelectEverything ? ( - - - - ) : null} ) : ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/empty_prompt.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/empty_prompt.tsx new file mode 100644 index 000000000000..256ab2a0bacd --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/empty_prompt.tsx @@ -0,0 +1,44 @@ +/* + * 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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const EmptyPrompt: React.FunctionComponent<{ + hasFleetAllPrivileges: boolean; + setEnrollmentFlyoutState: ( + value: React.SetStateAction<{ + isOpen: boolean; + selectedPolicyId?: string | undefined; + }> + ) => void; +}> = ({ hasFleetAllPrivileges, setEnrollmentFlyoutState }) => { + return ( + + + + } + actions={ + hasFleetAllPrivileges ? ( + setEnrollmentFlyoutState({ isOpen: true })} + > + + + ) : null + } + /> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx index 8965ae9bbc44..a511c2dc9f3d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -19,10 +19,13 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { AgentPolicy } from '../../../../types'; +import type { Agent, AgentPolicy } from '../../../../types'; import { AgentEnrollmentFlyout, SearchBar } from '../../../../components'; import { AGENTS_INDEX } from '../../../../constants'; +import { AgentBulkActions } from './bulk_actions'; +import type { SelectionMode } from './types'; + const statusFilters = [ { status: 'healthy', @@ -67,6 +70,12 @@ export const SearchAndFilterBar: React.FunctionComponent<{ onSelectedStatusChange: (selectedStatus: string[]) => void; showUpgradeable: boolean; onShowUpgradeableChange: (showUpgradeable: boolean) => void; + totalAgents: number; + totalInactiveAgents: number; + selectionMode: SelectionMode; + currentQuery: string; + selectedAgents: Agent[]; + refreshAgents: () => void; }> = ({ agentPolicies, draftKuery, @@ -78,6 +87,12 @@ export const SearchAndFilterBar: React.FunctionComponent<{ onSelectedStatusChange, showUpgradeable, onShowUpgradeableChange, + totalAgents, + totalInactiveAgents, + selectionMode, + currentQuery, + selectedAgents, + refreshAgents, }) => { const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); @@ -230,6 +245,16 @@ export const SearchAndFilterBar: React.FunctionComponent<{
+ + + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx index ff809d360e74..4e2f058596cf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx @@ -11,51 +11,41 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import type { Agent, SimplifiedAgentStatus } from '../../../../types'; import { AgentStatusBar } from './status_bar'; -import { AgentBulkActions } from './bulk_actions'; +import { AgentsSelectionStatus } from './agents_selection_status'; import {} from '@elastic/eui'; import { AgentStatusBadges } from './status_badges'; - -export type SelectionMode = 'manual' | 'query'; +import type { SelectionMode } from './types'; export const AgentTableHeader: React.FunctionComponent<{ agentStatus?: { [k in SimplifiedAgentStatus]: number }; showInactive: boolean; totalAgents: number; - totalInactiveAgents: number; selectableAgents: number; selectionMode: SelectionMode; setSelectionMode: (mode: SelectionMode) => void; - currentQuery: string; selectedAgents: Agent[]; setSelectedAgents: (agents: Agent[]) => void; - refreshAgents: () => void; }> = ({ agentStatus, totalAgents, - totalInactiveAgents, selectableAgents, selectionMode, setSelectionMode, - currentQuery, selectedAgents, setSelectedAgents, - refreshAgents, showInactive, }) => { return ( <> - diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_row_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_row_actions.tsx new file mode 100644 index 000000000000..be620f0044cd --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_row_actions.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { Agent, AgentPolicy } from '../../../../types'; +import { useAuthz, useLink, useKibanaVersion } from '../../../../hooks'; +import { ContextMenuActions } from '../../../../components'; +import { isAgentUpgradeable } from '../../../../services'; + +export const TableRowActions: React.FunctionComponent<{ + agent: Agent; + agentPolicy?: AgentPolicy; + onReassignClick: () => void; + onUnenrollClick: () => void; + onUpgradeClick: () => void; +}> = ({ agent, agentPolicy, onReassignClick, onUnenrollClick, onUpgradeClick }) => { + const { getHref } = useLink(); + const hasFleetAllPrivileges = useAuthz().fleet.all; + + const isUnenrolling = agent.status === 'unenrolling'; + const kibanaVersion = useKibanaVersion(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const menuItems = [ + + + , + ]; + + if (agentPolicy?.is_managed === false) { + menuItems.push( + { + onReassignClick(); + }} + disabled={!agent.active} + key="reassignPolicy" + > + + , + { + onUnenrollClick(); + }} + > + {isUnenrolling ? ( + + ) : ( + + )} + , + { + onUpgradeClick(); + }} + > + + + ); + } + return ( + setIsMenuOpen(isOpen)} + items={menuItems} + /> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/types.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/types.tsx new file mode 100644 index 000000000000..7e22c67b09ac --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/types.tsx @@ -0,0 +1,8 @@ +/* + * 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 type SelectionMode = 'manual' | 'query'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index d5757419e8ea..5776a163fd6a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -8,14 +8,11 @@ import React, { useState, useMemo, useCallback, useRef, useEffect, useContext } from 'react'; import { EuiBasicTable, - EuiButton, - EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, - EuiContextMenuItem, EuiIcon, EuiPortal, } from '@elastic/eui'; @@ -35,11 +32,7 @@ import { useKibanaVersion, useStartServices, } from '../../../hooks'; -import { - AgentEnrollmentFlyout, - AgentPolicySummaryLine, - ContextMenuActions, -} from '../../../components'; +import { AgentEnrollmentFlyout, AgentPolicySummaryLine } from '../../../components'; import { AgentStatusKueryHelper, isAgentUpgradeable } from '../../../services'; import { AGENTS_PREFIX, FLEET_SERVER_PACKAGE } from '../../../constants'; import { @@ -55,92 +48,13 @@ import { useFleetServerUnhealthy } from '../hooks/use_fleet_server_unhealthy'; import { agentFlyoutContext } from '..'; import { AgentTableHeader } from './components/table_header'; -import type { SelectionMode } from './components/bulk_actions'; +import type { SelectionMode } from './components/types'; import { SearchAndFilterBar } from './components/search_and_filter_bar'; +import { TableRowActions } from './components/table_row_actions'; +import { EmptyPrompt } from './components/empty_prompt'; const REFRESH_INTERVAL_MS = 30000; -const RowActions = React.memo<{ - agent: Agent; - agentPolicy?: AgentPolicy; - refresh: () => void; - onReassignClick: () => void; - onUnenrollClick: () => void; - onUpgradeClick: () => void; -}>(({ agent, agentPolicy, refresh, onReassignClick, onUnenrollClick, onUpgradeClick }) => { - const { getHref } = useLink(); - const hasFleetAllPrivileges = useAuthz().fleet.all; - - const isUnenrolling = agent.status === 'unenrolling'; - const kibanaVersion = useKibanaVersion(); - const [isMenuOpen, setIsMenuOpen] = useState(false); - const menuItems = [ - - - , - ]; - - if (agentPolicy?.is_managed === false) { - menuItems.push( - { - onReassignClick(); - }} - disabled={!agent.active} - key="reassignPolicy" - > - - , - { - onUnenrollClick(); - }} - > - {isUnenrolling ? ( - - ) : ( - - )} - , - { - onUpgradeClick(); - }} - > - - - ); - } - return ( - setIsMenuOpen(isOpen)} - items={menuItems} - /> - ); -}); - function safeMetadata(val: any) { if (typeof val !== 'string') { return '-'; @@ -479,10 +393,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { ? agentPoliciesIndexedById[agent.policy_id] : undefined; return ( - fetchData()} onReassignClick={() => setAgentToReassign(agent)} onUnenrollClick={() => setAgentToUnenroll(agent)} onUpgradeClick={() => setAgentToUpgrade(agent)} @@ -495,30 +408,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, ]; - const emptyPrompt = ( - - - - } - actions={ - hasFleetAllPrivileges ? ( - setEnrollmentFlyoutState({ isOpen: true })} - > - - - ) : null - } - /> - ); - return ( <> {enrollmentFlyout.isOpen ? ( @@ -592,6 +481,12 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { onSelectedStatusChange={setSelectedStatus} showUpgradeable={showUpgradeable} onShowUpgradeableChange={setShowUpgradeable} + totalAgents={totalAgents} + totalInactiveAgents={totalInactiveAgents} + selectionMode={selectionMode} + currentQuery={kuery} + selectedAgents={selectedAgents} + refreshAgents={() => fetchData()} /> @@ -599,12 +494,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { { if (tableRef?.current) { @@ -612,7 +505,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { setSelectionMode('manual'); } }} - refreshAgents={() => fetchData()} /> @@ -645,7 +537,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }} /> ) : ( - emptyPrompt + ) } items={ diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx index 566bc7f4363a..5e74b8672824 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx @@ -99,6 +99,7 @@ export const AgentReassignAgentPolicyModal: React.FunctionComponent = ({ return ( = ({ return ( = ({ return ( diff --git a/x-pack/plugins/fleet/public/mocks.ts b/x-pack/plugins/fleet/public/mocks.ts index 92b002a43764..8088f430ee81 100644 --- a/x-pack/plugins/fleet/public/mocks.ts +++ b/x-pack/plugins/fleet/public/mocks.ts @@ -5,8 +5,14 @@ * 2.0. */ +import type { EuiTheme } from '@kbn/kibana-react-plugin/common'; +import type { RecursivePartial } from '@elastic/eui/src/components/common'; + import { createStartMock } from './mock'; +export const getMockTheme = (partialTheme: RecursivePartial): EuiTheme => + partialTheme as EuiTheme; + export const fleetMock = { createStartMock, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 3ab363372567..7f21cf21000b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -635,7 +635,7 @@ describe('IndexPatternDimensionEditorPanel', () => { act(() => { wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') + .find('input[data-test-subj="column-label-edit"]') .simulate('change', { target: { value: 'New Label' } }); }); @@ -739,7 +739,7 @@ describe('IndexPatternDimensionEditorPanel', () => { act(() => { wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') + .find('input[data-test-subj="column-label-edit"]') .simulate('change', { target: { value: 'Sum of bytes' } }); }); diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts index ab6f2066e880..58ef8ce05c61 100644 --- a/x-pack/plugins/lens/public/mocks/index.ts +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -31,14 +31,14 @@ export type FrameMock = jest.Mocked; export const createMockFramePublicAPI = (): FrameMock => ({ datasourceLayers: {}, - dateRange: { fromDate: 'now-7d', toDate: 'now' }, + dateRange: { fromDate: '2022-03-17T08:25:00.000Z', toDate: '2022-04-17T08:25:00.000Z' }, }); export type FrameDatasourceMock = jest.Mocked; export const createMockFrameDatasourceAPI = (): FrameDatasourceMock => ({ datasourceLayers: {}, - dateRange: { fromDate: 'now-7d', toDate: 'now' }, + dateRange: { fromDate: '2022-03-17T08:25:00.000Z', toDate: '2022-04-17T08:25:00.000Z' }, query: { query: '', language: 'lucene' }, filters: [], }); diff --git a/x-pack/plugins/lens/public/shared_components/name_input.tsx b/x-pack/plugins/lens/public/shared_components/name_input.tsx index 0b65b2602162..9502c7df93d5 100644 --- a/x-pack/plugins/lens/public/shared_components/name_input.tsx +++ b/x-pack/plugins/lens/public/shared_components/name_input.tsx @@ -35,8 +35,9 @@ export const NameInput = ({ fullWidth > { handleInputChange(e.target.value); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 0d60db866a2c..8c6c6d9af22d 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -525,7 +525,7 @@ export interface OperationDescriptor extends Operation { export interface VisualizationConfigProps { layerId: string; - frame: Pick; + frame: FramePublicAPI; state: T; } diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx index 9492c980beac..f9f4b9da5342 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -7,7 +7,11 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { defaultAnnotationColor } from '@kbn/event-annotation-plugin/public'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotation, +} from '@kbn/event-annotation-plugin/public'; import { layerTypes } from '../../../common'; import type { FramePublicAPI, Visualization } from '../../types'; import { isHorizontalChart } from '../state_helpers'; @@ -29,6 +33,13 @@ export const defaultAnnotationLabel = i18n.translate('xpack.lens.xyChart.default defaultMessage: 'Event', }); +export const defaultRangeAnnotationLabel = i18n.translate( + 'xpack.lens.xyChart.defaultRangeAnnotationLabel', + { + defaultMessage: 'Event range', + } +); + export function getStaticDate(dataLayers: XYDataLayerConfig[], frame: FramePublicAPI) { const dataLayersId = dataLayers.map(({ layerId }) => layerId); const { activeData, dateRange } = frame; @@ -162,7 +173,9 @@ export const getAnnotationsAccessorColorConfig = (layer: XYAnnotationLayerConfig return { columnId: annotation.id, triggerIcon: annotation.isHidden ? ('invisible' as const) : ('color' as const), - color: annotation?.color || defaultAnnotationColor, + color: + annotation?.color || + (isRangeAnnotation(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor), }; }); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 931eacb5bf6b..1c73c455dfe9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -30,7 +30,7 @@ import { getReferenceLayers, getAnnotationsLayers, } from './visualization_helpers'; -import { getUniqueLabels, defaultAnnotationLabel } from './annotations/helpers'; +import { getUniqueLabels } from './annotations/helpers'; import { layerTypes } from '../../common'; export const getSortedAccessors = ( @@ -86,7 +86,7 @@ const simplifiedLayerExpression = { [layerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => ({ ...layer, hide: true, - yConfig: layer.yConfig?.map(({ lineWidth, ...rest }) => ({ + yConfig: layer.yConfig?.map(({ ...rest }) => ({ ...rest, lineWidth: 1, icon: undefined, @@ -96,12 +96,6 @@ const simplifiedLayerExpression = { [layerTypes.ANNOTATIONS]: (layer: XYAnnotationLayerConfig) => ({ ...layer, hide: true, - annotations: layer.annotations?.map(({ lineWidth, ...rest }) => ({ - ...rest, - lineWidth: 1, - icon: undefined, - textVisibility: false, - })), }), }; @@ -395,19 +389,7 @@ const annotationLayerToExpression = ( hide: [Boolean(layer.hide)], layerId: [layer.layerId], annotations: layer.annotations - ? layer.annotations.map( - (ann): Ast => - eventAnnotationService.toExpression({ - time: ann.key.timestamp, - label: ann.label || defaultAnnotationLabel, - textVisibility: ann.textVisibility, - icon: ann.icon, - lineStyle: ann.lineStyle, - lineWidth: ann.lineWidth, - color: ann.color, - isHidden: Boolean(ann.isHidden), - }) - ) + ? layer.annotations.map((ann): Ast => eventAnnotationService.toExpression(ann)) : [], }, }, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx index 6435f4c7ba2b..02a1858e5444 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -5,23 +5,115 @@ * 2.0. */ +import './index.scss'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDatePicker, EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { + EuiDatePicker, + EuiFormRow, + EuiSwitch, + EuiSwitchEvent, + EuiButtonGroup, + EuiFormLabel, + EuiFormControlLayout, + EuiText, + transparentize, +} from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; import moment from 'moment'; -import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common/types'; -import type { VisualizationDimensionEditorProps } from '../../../types'; -import { State, XYState, XYAnnotationLayerConfig } from '../../types'; +import { + EventAnnotationConfig, + PointInTimeEventAnnotationConfig, + RangeEventAnnotationConfig, +} from '@kbn/event-annotation-plugin/common/types'; +import { pick } from 'lodash'; +import { search } from '@kbn/data-plugin/public'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotation, +} from '@kbn/event-annotation-plugin/public'; +import Color from 'color'; +import { getDataLayers } from '../../visualization_helpers'; import { FormatFactory } from '../../../../common'; import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components'; import { isHorizontalChart } from '../../state_helpers'; -import { defaultAnnotationLabel } from '../../annotations/helpers'; +import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers'; import { ColorPicker } from '../color_picker'; import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings'; import { LineStyleSettings } from '../shared/line_style_settings'; import { updateLayer } from '..'; import { annotationsIconSet } from './icon_set'; +import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types'; +import { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types'; + +export const toRangeAnnotationColor = (color = defaultAnnotationColor) => { + return new Color(transparentize(color, 0.1)).hexa(); +}; + +export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => { + return new Color(transparentize(color, 1)).hex(); +}; + +export const getEndTimestamp = ( + startTime: string, + { activeData, dateRange }: FramePublicAPI, + dataLayers: XYDataLayerConfig[] +) => { + const startTimeNumber = moment(startTime).valueOf(); + const dateRangeFraction = + (moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1; + const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString(); + const dataLayersId = dataLayers.map(({ layerId }) => layerId); + if ( + !dataLayersId.length || + !activeData || + Object.entries(activeData) + .filter(([key]) => dataLayersId.includes(key)) + .every(([, { rows }]) => !rows || !rows.length) + ) { + return fallbackValue; + } + const xColumn = activeData?.[dataLayersId[0]].columns.find( + (column) => column.id === dataLayers[0].xAccessor + ); + if (!xColumn) { + return fallbackValue; + } + + const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval; + if (!dateInterval) return fallbackValue; + const intervalDuration = search.aggs.parseInterval(dateInterval); + if (!intervalDuration) return fallbackValue; + return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString(); +}; + +const sanitizeProperties = (annotation: EventAnnotationConfig) => { + if (isRangeAnnotation(annotation)) { + const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [ + 'label', + 'key', + 'id', + 'isHidden', + 'color', + 'outside', + ]); + return rangeAnnotation; + } else { + const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [ + 'id', + 'label', + 'key', + 'isHidden', + 'lineStyle', + 'lineWidth', + 'color', + 'icon', + 'textVisibility', + ]); + return lineAnnotation; + } +}; export const AnnotationsPanel = ( props: VisualizationDimensionEditorProps & { @@ -29,7 +121,7 @@ export const AnnotationsPanel = ( paletteService: PaletteRegistry; } ) => { - const { state, setState, layerId, accessor } = props; + const { state, setState, layerId, accessor, frame } = props; const isHorizontal = isHorizontalChart(state.layers); const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ @@ -42,19 +134,26 @@ export const AnnotationsPanel = ( (l) => l.layerId === layerId ) as XYAnnotationLayerConfig; - const currentAnnotations = localLayer.annotations?.find((c) => c.id === accessor); + const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor); + + const isRange = isRangeAnnotation(currentAnnotation); const setAnnotations = useCallback( - (annotations: Partial | undefined) => { - if (annotations == null) { + (annotation) => { + if (annotation == null) { return; } const newConfigs = [...(localLayer.annotations || [])]; const existingIndex = newConfigs.findIndex((c) => c.id === accessor); if (existingIndex !== -1) { - newConfigs[existingIndex] = { ...newConfigs[existingIndex], ...annotations }; + newConfigs[existingIndex] = sanitizeProperties({ + ...newConfigs[existingIndex], + ...annotation, + }); } else { - return; // that should never happen because annotations are created before annotations panel is opened + throw new Error( + 'should never happen because annotation is created before config panel is opened' + ); } setLocalState(updateLayer(localState, { ...localLayer, annotations: newConfigs }, index)); }, @@ -68,21 +167,97 @@ export const AnnotationsPanel = ( defaultMessage: 'Placement', })} > - { - if (date) { - setAnnotations({ - key: { - ...(currentAnnotations?.key || { type: 'point_in_time' }), - timestamp: date.toISOString(), - }, - }); - } - }} - label={i18n.translate('xpack.lens.xyChart.annotationDate', { - defaultMessage: 'Annotation date', - })} + {isRange ? ( + <> + { + if (date) { + const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf(); + if (currentEndTime < date.valueOf()) { + const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf(); + const dif = currentEndTime - currentStartTime; + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'range' }), + timestamp: date.toISOString(), + endTimestamp: moment(date.valueOf() + dif).toISOString(), + }, + }); + } else { + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'range' }), + timestamp: date.toISOString(), + }, + }); + } + } + }} + label={i18n.translate('xpack.lens.xyChart.annotationDate', { + defaultMessage: 'Annotation date', + })} + /> + { + if (date) { + const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf(); + if (currentStartTime > date.valueOf()) { + const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf(); + const dif = currentEndTime - currentStartTime; + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'range' }), + endTimestamp: date.toISOString(), + timestamp: moment(date.valueOf() - dif).toISOString(), + }, + }); + } else { + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'range' }), + endTimestamp: date.toISOString(), + }, + }); + } + } + }} + /> + + ) : ( + { + if (date) { + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'point_in_time' }), + timestamp: date.toISOString(), + }, + }); + } + }} + /> + )} + + { setAnnotations({ label: value }); }} /> - - - + {!isRange && ( + + )} + {!isRange && ( + + )} + {!isRange && ( + + )} + + {isRange && ( + + { + setAnnotations({ + outside: id === `lens_xyChart_fillStyle_outside`, + }); + }} + isFullWidth + /> + + )} + setAnnotations({ isHidden: ev.target.checked })} /> @@ -134,25 +363,114 @@ export const AnnotationsPanel = ( ); }; -const ConfigPanelDatePicker = ({ +const ConfigPanelApplyAsRangeSwitch = ({ + annotation, + onChange, + frame, + state, +}: { + annotation?: EventAnnotationConfig; + onChange: (annotations: Partial | undefined) => void; + frame: FramePublicAPI; + state: XYState; +}) => { + const isRange = isRangeAnnotation(annotation); + return ( + + + {i18n.translate('xpack.lens.xyChart.applyAsRange', { + defaultMessage: 'Apply as range', + })} + + } + checked={isRange} + onChange={() => { + if (isRange) { + const newPointAnnotation: PointInTimeEventAnnotationConfig = { + key: { + type: 'point_in_time', + timestamp: annotation.key.timestamp, + }, + id: annotation.id, + label: + annotation.label === defaultRangeAnnotationLabel + ? defaultAnnotationLabel + : annotation.label, + color: toLineAnnotationColor(annotation.color), + isHidden: annotation.isHidden, + }; + onChange(newPointAnnotation); + } else if (annotation) { + const fromTimestamp = moment(annotation?.key.timestamp); + const dataLayers = getDataLayers(state.layers); + const newRangeAnnotation: RangeEventAnnotationConfig = { + key: { + type: 'range', + timestamp: annotation.key.timestamp, + endTimestamp: getEndTimestamp(fromTimestamp.toISOString(), frame, dataLayers), + }, + id: annotation.id, + label: + annotation.label === defaultAnnotationLabel + ? defaultRangeAnnotationLabel + : annotation.label, + color: toRangeAnnotationColor(annotation.color), + isHidden: annotation.isHidden, + }; + onChange(newRangeAnnotation); + } + }} + compressed + /> + + ); +}; + +const ConfigPanelRangeDatePicker = ({ value, label, + prependLabel, onChange, + dataTestSubj = 'lnsXY_annotation_date_picker', }: { value: moment.Moment; - label: string; + prependLabel?: string; + label?: string; onChange: (val: moment.Moment | null) => void; + dataTestSubj?: string; }) => { return ( - - + + {prependLabel ? ( + {prependLabel} + } + > + + + ) : ( + + )} ); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.scss b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.scss new file mode 100644 index 000000000000..3a0f4b944aa6 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.scss @@ -0,0 +1,12 @@ +.lnsRowCompressedMargin+.lnsRowCompressedMargin { + margin-top: $euiSizeS; +} + +.lnsConfigPanelNoPadding { + padding: 0; +} + +.lnsConfigPanelDate__label { + min-width: 56px; // makes both labels ("from" and "to") the same width + text-align: center; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx new file mode 100644 index 000000000000..6194a7f0da12 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { AnnotationsPanel } from '.'; +import { FramePublicAPI } from '../../../types'; +import { layerTypes } from '../../..'; +import { createMockFramePublicAPI } from '../../../mocks'; +import { State } from '../../types'; +import { Position } from '@elastic/charts'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import moment from 'moment'; + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + +const customLineStaticAnnotation = { + id: 'ann1', + key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' }, + label: 'Event', + icon: 'triangle', + color: 'red', + lineStyle: 'dashed' as const, + lineWidth: 3, +}; + +describe('AnnotationsPanel', () => { + let frame: FramePublicAPI; + + function testState(): State { + return { + legend: { isVisible: true, position: Position.Right }, + valueLabels: 'hide', + preferredSeriesType: 'bar', + layers: [ + { + layerType: layerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [customLineStaticAnnotation], + }, + ], + }; + } + + beforeEach(() => { + frame = createMockFramePublicAPI(); + frame.datasourceLayers = {}; + }); + describe('Dimension Editor', () => { + test('shows correct options for line annotations', () => { + const state = testState(); + const component = mount( + + ); + + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-time"]').prop('selected') + ).toEqual(moment('2022-03-18T08:25:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-fromTime"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-toTime"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') + ).toEqual(false); + expect( + component.find('EuiFieldText[data-test-subj="column-label-edit"]').prop('value') + ).toEqual('Event'); + expect( + component.find('EuiComboBox[data-test-subj="lns-icon-select"]').prop('selectedOptions') + ).toEqual([{ label: 'Triangle', value: 'triangle' }]); + expect(component.find('TextDecorationSetting').exists()).toBeTruthy(); + expect(component.find('LineStyleSettings').exists()).toBeTruthy(); + expect( + component.find('EuiButtonGroup[data-test-subj="lns-xyAnnotation-fillStyle"]').exists() + ).toBeFalsy(); + }); + test('shows correct options for range annotations', () => { + const state = testState(); + state.layers[0] = { + annotations: [ + { + color: 'red', + icon: 'triangle', + id: 'ann1', + isHidden: undefined, + key: { + endTimestamp: '2022-03-21T10:49:00.000Z', + timestamp: '2022-03-18T08:25:00.000Z', + type: 'range', + }, + label: 'Event range', + lineStyle: 'dashed', + lineWidth: 3, + }, + ], + layerId: 'annotation', + layerType: 'annotations', + }; + const component = mount( + + ); + + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-fromTime"]').prop('selected') + ).toEqual(moment('2022-03-18T08:25:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-toTime"]').prop('selected') + ).toEqual(moment('2022-03-21T10:49:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-time"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') + ).toEqual(true); + expect( + component.find('EuiFieldText[data-test-subj="column-label-edit"]').prop('value') + ).toEqual('Event range'); + expect(component.find('EuiComboBox[data-test-subj="lns-icon-select"]').exists()).toBeFalsy(); + expect(component.find('TextDecorationSetting').exists()).toBeFalsy(); + expect(component.find('LineStyleSettings').exists()).toBeFalsy(); + expect(component.find('[data-test-subj="lns-xyAnnotation-fillStyle"]').exists()).toBeTruthy(); + }); + + test('calculates correct endTimstamp and transparent color when switching for range annotation and back', () => { + const state = testState(); + const setState = jest.fn(); + const component = mount( + + ); + component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); + + expect(setState).toBeCalledWith({ + ...state, + layers: [ + { + annotations: [ + { + color: '#FF00001A', + id: 'ann1', + isHidden: undefined, + label: 'Event range', + key: { + endTimestamp: '2022-03-21T10:49:00.000Z', + timestamp: '2022-03-18T08:25:00.000Z', + type: 'range', + }, + }, + ], + layerId: 'annotation', + layerType: 'annotations', + }, + ], + }); + component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); + expect(setState).toBeCalledWith({ + ...state, + layers: [ + { + annotations: [ + { + color: '#FF0000', + id: 'ann1', + isHidden: undefined, + key: { + timestamp: '2022-03-18T08:25:00.000Z', + type: 'point_in_time', + }, + label: 'Event', + }, + ], + layerId: 'annotation', + layerType: 'annotations', + }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx index 9024b0f4f593..00f03f261ef7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -5,23 +5,26 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; +import chroma from 'chroma-js'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiColorPicker, EuiColorPickerProps, EuiToolTip, EuiIcon } from '@elastic/eui'; +import { + EuiFormRow, + EuiColorPicker, + EuiColorPickerProps, + EuiToolTip, + EuiIcon, + euiPaletteColorBlind, +} from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; -import { defaultAnnotationColor } from '@kbn/event-annotation-plugin/public'; import type { VisualizationDimensionEditorProps } from '../../types'; -import { State, XYDataLayerConfig } from '../types'; +import { State } from '../types'; import { FormatFactory } from '../../../common'; import { getSeriesColor } from '../state_helpers'; -import { - defaultReferenceLineColor, - getAccessorColorConfig, - getColorAssignments, -} from '../color_assignment'; +import { getAccessorColorConfig, getColorAssignments } from '../color_assignment'; import { getSortedAccessors } from '../to_expression'; import { TooltipWrapper } from '../../shared_components'; -import { isReferenceLayer, isAnnotationsLayer, getDataLayers } from '../visualization_helpers'; +import { getDataLayers, isDataLayer } from '../visualization_helpers'; const tooltipContent = { auto: i18n.translate('xpack.lens.configPanel.color.tooltip.auto', { @@ -47,6 +50,8 @@ export const ColorPicker = ({ disableHelpTooltip, disabled, setConfig, + showAlpha, + defaultColor, }: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; paletteService: PaletteRegistry; @@ -54,6 +59,8 @@ export const ColorPicker = ({ disableHelpTooltip?: boolean; disabled?: boolean; setConfig: (config: { color?: string }) => void; + showAlpha?: boolean; + defaultColor?: string; }) => { const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; @@ -61,38 +68,47 @@ export const ColorPicker = ({ const overwriteColor = getSeriesColor(layer, accessor); const currentColor = useMemo(() => { if (overwriteColor || !frame.activeData) return overwriteColor; - if (isReferenceLayer(layer)) { - return defaultReferenceLineColor; - } else if (isAnnotationsLayer(layer)) { - return defaultAnnotationColor; + if (defaultColor) { + return defaultColor; } - - const dataLayer: XYDataLayerConfig = layer; - const sortedAccessors: string[] = getSortedAccessors( - frame.datasourceLayers[layer.layerId] ?? layer.accessors, - layer - ); - - const colorAssignments = getColorAssignments( - getDataLayers(state.layers), - { tables: frame.activeData ?? {} }, - formatFactory - ); - const mappedAccessors = getAccessorColorConfig( - colorAssignments, - frame, - { - ...dataLayer, - accessors: sortedAccessors.filter((sorted) => dataLayer.accessors.includes(sorted)), - }, - paletteService - ); - - return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; - }, [overwriteColor, frame, layer, state.layers, formatFactory, paletteService, accessor]); + if (isDataLayer(layer)) { + const sortedAccessors: string[] = getSortedAccessors( + frame.datasourceLayers[layer.layerId] ?? layer.accessors, + layer + ); + const colorAssignments = getColorAssignments( + getDataLayers(state.layers), + { tables: frame.activeData ?? {} }, + formatFactory + ); + const mappedAccessors = getAccessorColorConfig( + colorAssignments, + frame, + { + ...layer, + accessors: sortedAccessors.filter((sorted) => layer.accessors.includes(sorted)), + }, + paletteService + ); + return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; + } + }, [ + overwriteColor, + frame, + paletteService, + state.layers, + accessor, + formatFactory, + layer, + defaultColor, + ]); const [color, setColor] = useState(currentColor); + useEffect(() => { + setColor(currentColor); + }, [currentColor]); + const handleColor: EuiColorPickerProps['onChange'] = (text, output) => { setColor(text); if (output.isValid || text === '') { @@ -107,8 +123,11 @@ export const ColorPicker = ({ defaultMessage: 'Series color', }); + const currentColorAlpha = color ? chroma(color).alpha() : 1; + const colorPicker = ( chroma(c).alpha(currentColorAlpha).hex()) + } /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx index e44fd053c7c8..e25c191d2bec 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx @@ -26,6 +26,7 @@ import { } from '../shared/marker_decoration_settings'; import { LineStyleSettings } from '../shared/line_style_settings'; import { referenceLineIconsSet } from './icon_set'; +import { defaultReferenceLineColor } from '../../color_assignment'; export const ReferenceLinePanel = ( props: VisualizationDimensionEditorProps & { @@ -93,6 +94,7 @@ export const ReferenceLinePanel = ( ({ value, onChange, customIconSet, + defaultIcon = 'empty', }: { value?: Icon; onChange: (newIcon: Icon) => void; customIconSet: IconSet; + defaultIcon?: string; }) { const selectedIcon = customIconSet.find((option) => value === option.value) || - customIconSet.find((option) => option.value === 'empty')!; + customIconSet.find((option) => option.value === defaultIcon)!; return ( { onChange(selection[0].value!); }} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx index a5f740ce9a99..64b00ef24616 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx @@ -132,10 +132,12 @@ export function IconSelectSetting({ currentConfig, setConfig, customIconSet, + defaultIcon = 'empty', }: { currentConfig?: MarkerDecorationConfig; setConfig: (config: MarkerDecorationConfig) => void; customIconSet: IconSet; + defaultIcon?: string; }) { return ( ({ })} > { diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts index 9c9d993e0a34..1888265c124f 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -344,6 +344,7 @@ export class BaseRule { if (ccs) { globalState.push(`ccs:${ccs}`); } - return `${Globals.app.url}/app/monitoring#/${link}?_g=(${globalState.toString()})`; + + return `${Globals.app.url ?? ''}/app/monitoring#/${link}?_g=(${globalState.toString()})`; } } diff --git a/x-pack/plugins/monitoring/server/static_globals.ts b/x-pack/plugins/monitoring/server/static_globals.ts index e601dd0c5515..429ec8693a3e 100644 --- a/x-pack/plugins/monitoring/server/static_globals.ts +++ b/x-pack/plugins/monitoring/server/static_globals.ts @@ -6,7 +6,6 @@ */ import { CoreSetup, ElasticsearchClient, Logger, PluginInitializerContext } from '@kbn/core/server'; -import url from 'url'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { MonitoringConfig } from './config'; import { PluginsSetup } from './types'; @@ -32,7 +31,7 @@ export type EndpointTypes = export type ClientParams = estypes.SearchRequest | undefined; interface IAppGlobals { - url: string; + url?: string; isCloud: boolean; config: MonitoringConfig; getLogger: GetLogger; @@ -79,11 +78,8 @@ export class Globals { return body; }); - const { protocol, hostname, port } = coreSetup.http.getServerInfo(); - const pathname = coreSetup.http.basePath.serverBasePath; - Globals._app = { - url: url.format({ protocol, hostname, port, pathname }), + url: coreSetup.http.basePath.publicBaseUrl, isCloud: setupPlugins.cloud?.isCloudEnabled || false, config, getLogger, diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts index 369b1fff5fe3..04dce4e60a09 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts @@ -71,6 +71,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_INDEX, body: action, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) @@ -98,6 +99,7 @@ export const indexEndpointAndFleetActionsForHost = async ( .index({ index: ENDPOINT_ACTIONS_INDEX, body: endpointActionsBody, + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise), ]); @@ -125,6 +127,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_RESULTS_INDEX, body: actionResponse, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) @@ -159,6 +162,7 @@ export const indexEndpointAndFleetActionsForHost = async ( .index({ index: ENDPOINT_ACTION_RESPONSES_INDEX, body: endpointActionResponseBody, + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise), ]); @@ -197,6 +201,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_INDEX, body: action, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) @@ -221,6 +226,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_INDEX, body: action1, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) @@ -230,6 +236,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_INDEX, body: action2, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index 2f304351dc43..30d75b30a11b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -165,6 +165,10 @@ export async function indexEndpointHostDocs({ // Update the Host metadata record with the ID of the "real" policy along with the enrolled agent id hostMetadata = { ...hostMetadata, + agent: { + ...hostMetadata.agent, + id: enrolledAgent?.id ?? hostMetadata.agent.id, + }, elastic: { ...hostMetadata.elastic, agent: { @@ -201,6 +205,7 @@ export async function indexEndpointHostDocs({ index: metadataIndex, body: hostMetadata, op_type: 'create', + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise); @@ -214,6 +219,7 @@ export async function indexEndpointHostDocs({ index: policyResponseIndex, body: hostPolicyResponse, op_type: 'create', + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts index 70b1e1c52a77..b051eff37edc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts @@ -47,6 +47,7 @@ export const indexFleetAgentForHost = async ( fleetAgentGenerator: FleetAgentGenerator = defaultFleetAgentGenerator ): Promise => { const agentDoc = fleetAgentGenerator.generateEsHit({ + _id: endpointHost.agent.id, _source: { agent: { id: endpointHost.agent.id, @@ -75,6 +76,7 @@ export const indexFleetAgentForHost = async ( id: agentDoc._id, body: agentDoc._source, op_type: 'create', + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_server.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_server.ts index c11c2025ee88..8b8a15a1164e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_server.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_server.ts @@ -31,6 +31,7 @@ export const enableFleetServerIfNecessary = async (esClient: Client, version: st await esClient .index({ index: FLEET_SERVER_SERVERS_INDEX, + refresh: 'wait_for', body: { agent: { id: '12988155-475c-430d-ac89-84dc84b67cd1', diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index a571c7848bdf..5a6b20550f22 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -464,16 +464,17 @@ export class EndpointDocGenerator extends BaseDataGenerator { const agentVersion = this.randomVersion(); const minCapabilitiesVersion = '7.15.0'; const capabilities = ['isolation']; + const agentId = this.seededUUIDv4(); return { agent: { version: agentVersion, - id: this.seededUUIDv4(), + id: agentId, type: 'endpoint', }, elastic: { agent: { - id: this.seededUUIDv4(), + id: agentId, }, }, host: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 57cdaca1f214..d8dc87885a1f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -424,16 +424,16 @@ describe('endpoint list middleware', () => { path: expect.any(String), query: { agent_ids: [ - '6db499e5-4927-4350-abb8-d8318e7d0eec', - 'c082dda9-1847-4997-8eda-f1192d95bec3', - '8aa1cd61-cc25-4783-afb5-0eefc4919c07', - '47fe24c1-7370-419a-9732-3ff38bf41272', - '0d2b2fa7-a9cd-49fc-ad5f-0252c642290e', - 'f480092d-0445-4bf3-9c96-8a3d5cb97824', - '3850e676-0940-4c4b-aaca-571bd1bc66d9', - '46efcc7a-086a-47a3-8f09-c4ecd6d2d917', - 'afa55826-b81b-4440-a2ac-0644d77a3fc6', - '25b49e50-cb5c-43df-824f-67b8cf697d9d', + '0dc3661d-6e67-46b0-af39-6f12b025fcb0', + 'a8e32a61-2685-47f0-83eb-edf157b8e616', + '37e219a8-fe16-4da9-bf34-634c5824b484', + '2484eb13-967e-4491-bf83-dffefdfe607c', + '0bc08ef6-6d6a-4113-92f2-b97811187c63', + 'f4127d87-b567-4a6e-afa6-9a1c7dc95f01', + 'f9ab5b8c-a43e-4e80-99d6-11570845a697', + '406c4b6a-ca57-4bd1-bc66-d9d999df3e70', + '2da1dd51-f7af-4f0e-b64c-e7751c74b0e7', + '89a94ea4-073c-4cb6-90a2-500805837027', ], }, }); diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap index dca100efcc83..b033febcd1ac 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap @@ -15,11 +15,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "powershell.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "powershell.exe", + "name": "lsass.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -33,11 +33,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "powershell.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "powershell.exe", + "name": "lsass.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -58,11 +58,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "lsass.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "lsass.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -73,11 +73,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "powershell.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "powershell.exe", + "name": "lsass.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -88,11 +88,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "C", - "process.name": "iexlorer.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "A", }, "id": "C", - "name": "iexlorer.exe", + "name": "lsass.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -103,11 +103,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "I", - "process.name": "explorer.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "A", }, "id": "I", - "name": "explorer.exe", + "name": "notepad.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -118,11 +118,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "D", - "process.name": "mimikatz.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "B", }, "id": "D", - "name": "mimikatz.exe", + "name": "lsass.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -133,11 +133,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "E", - "process.name": "powershell.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "B", }, "id": "E", - "name": "powershell.exe", + "name": "mimikatz.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -148,11 +148,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "F", - "process.name": "explorer.exe", + "process.name": "powershell.exe", "process.parent.entity_id": "C", }, "id": "F", - "name": "explorer.exe", + "name": "powershell.exe", "parent": "C", "stats": Object { "byCategory": Object {}, @@ -178,11 +178,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "H", - "process.name": "lsass.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "G", }, "id": "H", - "name": "lsass.exe", + "name": "notepad.exe", "parent": "G", "stats": Object { "byCategory": Object {}, @@ -439,11 +439,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "lsass.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "lsass.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -457,11 +457,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "powershell.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "powershell.exe", + "name": "lsass.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -475,11 +475,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "C", - "process.name": "iexlorer.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "A", }, "id": "C", - "name": "iexlorer.exe", + "name": "lsass.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -493,11 +493,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "I", - "process.name": "explorer.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "A", }, "id": "I", - "name": "explorer.exe", + "name": "notepad.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -511,11 +511,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "D", - "process.name": "mimikatz.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "B", }, "id": "D", - "name": "mimikatz.exe", + "name": "lsass.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -529,11 +529,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "E", - "process.name": "powershell.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "B", }, "id": "E", - "name": "powershell.exe", + "name": "mimikatz.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -547,11 +547,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "F", - "process.name": "explorer.exe", + "process.name": "powershell.exe", "process.parent.entity_id": "C", }, "id": "F", - "name": "explorer.exe", + "name": "powershell.exe", "parent": "C", "stats": Object { "byCategory": Object {}, @@ -583,11 +583,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "H", - "process.name": "lsass.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "G", }, "id": "H", - "name": "lsass.exe", + "name": "notepad.exe", "parent": "G", "stats": Object { "byCategory": Object {}, @@ -608,11 +608,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "explorer.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "explorer.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -623,11 +623,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "iexlorer.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "iexlorer.exe", + "name": "mimikatz.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -661,11 +661,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "explorer.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "explorer.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -679,11 +679,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "iexlorer.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "iexlorer.exe", + "name": "mimikatz.exe", "parent": "A", "stats": Object { "byCategory": Object {}, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_monitor_list.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_monitor_list.ts new file mode 100644 index 000000000000..e0899571f38b --- /dev/null +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_monitor_list.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useReducer, Reducer } from 'react'; +import { useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { getMonitors } from '../../../state/actions'; +import { ConfigKey } from '../../../../../common/constants/monitor_management'; +import { MonitorManagementListPageState } from '../monitor_list/monitor_list'; + +export function useMonitorList() { + const dispatch = useDispatch(); + + const [pageState, dispatchPageAction] = useReducer( + monitorManagementPageReducer, + { + pageIndex: 1, // saved objects page index is base 1 + pageSize: 10, + sortOrder: 'asc', + sortField: `${ConfigKey.NAME}.keyword`, + } + ); + + const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState; + + const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>(); + + useEffect(() => { + if (viewType === 'all') { + dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortOrder, sortField })); + } + }, [dispatch, pageIndex, pageSize, sortField, sortOrder, viewType, pageState]); + + return { + pageState, + dispatchPageAction, + viewType, + }; +} + +export type MonitorManagementPageAction = + | { + type: 'update'; + payload: MonitorManagementListPageState; + } + | { type: 'refresh' }; + +const monitorManagementPageReducer: Reducer< + MonitorManagementListPageState, + MonitorManagementPageAction +> = (state: MonitorManagementListPageState, action: MonitorManagementPageAction) => { + switch (action.type) { + case 'update': + return { + ...state, + ...action.payload, + }; + case 'refresh': + return { ...state }; + default: + throw new Error(`Action "${(action as MonitorManagementPageAction)?.type}" not recognizable`); + } +}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx index 7191cd71b0a3..727f4f6dee72 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx @@ -5,32 +5,29 @@ * 2.0. */ -import React, { useEffect, useReducer, useCallback, Reducer } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { useCallback, Dispatch } from 'react'; +import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { useTrackPageview } from '@kbn/observability-plugin/public'; -import { ConfigKey } from '../../../../../common/runtime_types'; -import { getMonitors } from '../../../state/actions'; import { monitorManagementListSelector } from '../../../state/selectors'; -import { MonitorManagementListPageState } from './monitor_list'; import { MonitorAsyncError } from './monitor_async_error'; import { useInlineErrors } from '../hooks/use_inline_errors'; import { MonitorListTabs } from './list_tabs'; import { AllMonitors } from './all_monitors'; import { InvalidMonitors } from './invalid_monitors'; import { useInvalidMonitors } from '../hooks/use_invalid_monitors'; +import { MonitorManagementListPageState } from './monitor_list'; +import { MonitorManagementPageAction } from '../hooks/use_monitor_list'; -export const MonitorListContainer: React.FC = () => { - const [pageState, dispatchPageAction] = useReducer( - monitorManagementPageReducer, - { - pageIndex: 1, // saved objects page index is base 1 - pageSize: 10, - sortOrder: 'asc', - sortField: `${ConfigKey.NAME}.keyword`, - } - ); - +export const MonitorListContainer = ({ + isEnabled, + pageState, + dispatchPageAction, +}: { + isEnabled?: boolean; + pageState: MonitorManagementListPageState; + dispatchPageAction: Dispatch; +}) => { const onPageStateChange = useCallback( (state) => { dispatchPageAction({ type: 'update', payload: state }); @@ -45,11 +42,8 @@ export const MonitorListContainer: React.FC = () => { useTrackPageview({ app: 'uptime', path: 'manage-monitors' }); useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 }); - const dispatch = useDispatch(); const monitorList = useSelector(monitorManagementListSelector); - const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState; - const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>(); const { errorSummaries, loading, count } = useInlineErrors({ onlyInvalidMonitors: viewType === 'invalid', @@ -57,14 +51,12 @@ export const MonitorListContainer: React.FC = () => { sortOrder: pageState.sortOrder, }); - useEffect(() => { - if (viewType === 'all') { - dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortField, sortOrder })); - } - }, [dispatch, pageState, pageIndex, pageSize, sortField, sortOrder, viewType]); - const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries); + if (!isEnabled && monitorList.list.total === 0) { + return null; + } + return ( <> @@ -95,27 +87,3 @@ export const MonitorListContainer: React.FC = () => { ); }; - -type MonitorManagementPageAction = - | { - type: 'update'; - payload: MonitorManagementListPageState; - } - | { type: 'refresh' }; - -const monitorManagementPageReducer: Reducer< - MonitorManagementListPageState, - MonitorManagementPageAction -> = (state: MonitorManagementListPageState, action: MonitorManagementPageAction) => { - switch (action.type) { - case 'update': - return { - ...state, - ...action.payload, - }; - case 'refresh': - return { ...state }; - default: - throw new Error(`Action "${(action as MonitorManagementPageAction)?.type}" not recognizable`); - } -}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx index 97013b990518..ba243388c8a3 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx @@ -7,11 +7,9 @@ import React, { useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { EuiCallOut, EuiButton, EuiSpacer, EuiLink } from '@elastic/eui'; import { useTrackPageview } from '@kbn/observability-plugin/public'; -import { ConfigKey } from '../../../../common/runtime_types'; -import { getMonitors } from '../../state/actions'; import { monitorManagementListSelector } from '../../state/selectors'; import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs'; import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container'; @@ -20,12 +18,12 @@ import { useEnablement } from '../../components/monitor_management/hooks/use_ena import { useLocations } from '../../components/monitor_management/hooks/use_locations'; import { Loader } from '../../components/monitor_management/loader/loader'; import { ERROR_HEADING_LABEL } from './content'; +import { useMonitorList } from '../../components/monitor_management/hooks/use_monitor_list'; export const MonitorManagementPage: React.FC = () => { useTrackPageview({ app: 'uptime', path: 'manage-monitors' }); useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 }); useMonitorManagementBreadcrumbs(); - const dispatch = useDispatch(); const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); const { @@ -40,19 +38,6 @@ export const MonitorManagementPage: React.FC = () => { const isEnabledRef = useRef(isEnabled); - useEffect(() => { - if (monitorList.total === null) { - dispatch( - getMonitors({ - page: 1, // saved objects page index is base 1 - perPage: 10, - sortOrder: 'asc', - sortField: `${ConfigKey.NAME}.keyword`, - }) - ); - } - }, [dispatch, monitorList.total]); - useEffect(() => { if (!isEnabled && isEnabledRef.current === true) { /* shift focus to enable button when enable toggle disappears. Prevent @@ -62,10 +47,14 @@ export const MonitorManagementPage: React.FC = () => { isEnabledRef.current = Boolean(isEnabled); }, [isEnabled]); + const { pageState, dispatchPageAction } = useMonitorList(); + + const showEmptyState = isEnabled !== undefined && monitorList.total === 0; + return ( <> { ) : null} - {isEnabled || (!isEnabled && monitorList.total) ? : null} + - {isEnabled !== undefined && monitorList.total === 0 && ( - - )} + {showEmptyState && } ); }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/service_allowed_wrapper.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/service_allowed_wrapper.tsx index cda0e8a6cdbe..ff74fd97f0b3 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/service_allowed_wrapper.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/service_allowed_wrapper.tsx @@ -13,15 +13,6 @@ import { useSyntheticsServiceAllowed } from '../../components/monitor_management export const ServiceAllowedWrapper: React.FC = ({ children }) => { const { isAllowed, signupUrl, loading } = useSyntheticsServiceAllowed(); - if (loading) { - return ( - } - title={

{LOADING_MONITOR_MANAGEMENT_LABEL}

} - /> - ); - } - // checking for explicit false if (isAllowed === false) { return ( @@ -37,7 +28,17 @@ export const ServiceAllowedWrapper: React.FC = ({ children }) => { ); } - return <>{children}; + return ( + <> + {loading && ( + } + title={

{LOADING_MONITOR_MANAGEMENT_LABEL}

} + /> + )} +
{children}
+ + ); }; const REQUEST_ACCESS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.requestAccess', { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/effects/monitor_management.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/effects/monitor_management.ts index 60a667672178..b5ee599b0a4f 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/state/effects/monitor_management.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/effects/monitor_management.ts @@ -35,7 +35,7 @@ import { import { fetchEffectFactory } from './fetch_effect'; export function* fetchMonitorManagementEffect() { - yield takeLatest( + yield takeLeading( getMonitors, fetchEffectFactory(fetchMonitorManagementList, getMonitorsSuccess, getMonitorsFailure) ); @@ -47,7 +47,7 @@ export function* fetchMonitorManagementEffect() { getServiceLocationsFailure ) ); - yield takeLatest( + yield takeLeading( getSyntheticsEnablement, fetchEffectFactory( fetchGetSyntheticsEnablement, diff --git a/x-pack/plugins/synthetics/server/lib/synthetics_service/formatters/browser.ts b/x-pack/plugins/synthetics/server/lib/synthetics_service/formatters/browser.ts index 616d764c50ab..876827e453ad 100644 --- a/x-pack/plugins/synthetics/server/lib/synthetics_service/formatters/browser.ts +++ b/x-pack/plugins/synthetics/server/lib/synthetics_service/formatters/browser.ts @@ -10,6 +10,21 @@ import { BrowserFields, ConfigKey } from '../../../../common/runtime_types/monit export type BrowserFormatMap = Record; +const throttlingFormatter: Formatter = (fields) => { + if (!fields[ConfigKey.IS_THROTTLING_ENABLED]) return false; + + const getThrottlingValue = (v: string | undefined, suffix: 'd' | 'u' | 'l') => + v !== '' && v !== undefined ? `${v}${suffix}` : null; + + return [ + getThrottlingValue(fields[ConfigKey.DOWNLOAD_SPEED], 'd'), + getThrottlingValue(fields[ConfigKey.UPLOAD_SPEED], 'u'), + getThrottlingValue(fields[ConfigKey.LATENCY], 'l'), + ] + .filter((v) => v !== null) + .join('/'); +}; + export const browserFormatters: BrowserFormatMap = { [ConfigKey.METADATA]: (fields) => objectFormatter(fields[ConfigKey.METADATA]), [ConfigKey.URLS]: null, @@ -31,12 +46,7 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE]: null, [ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]: null, [ConfigKey.IS_THROTTLING_ENABLED]: null, - [ConfigKey.THROTTLING_CONFIG]: (fields) => { - if (fields[ConfigKey.IS_THROTTLING_ENABLED] === false) { - return false; - } - return fields[ConfigKey.THROTTLING_CONFIG] ?? false; - }, + [ConfigKey.THROTTLING_CONFIG]: (fields) => throttlingFormatter(fields), [ConfigKey.DOWNLOAD_SPEED]: null, [ConfigKey.UPLOAD_SPEED]: null, [ConfigKey.LATENCY]: null, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index cf0c95b863cf..66cae51a7b41 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -7490,7 +7490,6 @@ "xpack.apm.filter.environment.allLabel": "Tous", "xpack.apm.filter.environment.label": "Environnement", "xpack.apm.filter.environment.notDefinedLabel": "Non défini", - "xpack.apm.filter.environment.selectEnvironmentLabel": "Sélectionner l'environnement", "xpack.apm.fleet_integration.settings.advancedOptionsLavel": "Options avancées", "xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentHelpText": "Noms d'agents autorisés pour l'accès anonyme.", "xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentLabel": "Agents autorisés", @@ -7717,7 +7716,6 @@ "xpack.apm.propertiesTable.tabs.metadataLabel": "Métadonnées", "xpack.apm.propertiesTable.tabs.timelineLabel": "Chronologie", "xpack.apm.searchInput.filter": "Filtrer…", - "xpack.apm.selectCustomOptionText": "Ajouter \\{searchValue\\} en tant que nouvelle option", "xpack.apm.selectPlaceholder": "Sélectionner une option :", "xpack.apm.serviceDependencies.breakdownChartTitle": "Temps consacré par dépendance", "xpack.apm.serviceDetails.dependenciesTabLabel": "Dépendances", @@ -11922,7 +11920,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action": "Gérer le groupe", "xpack.enterpriseSearch.workplaceSearch.groups.newGroupSavedSuccess": "{groupName} créé avec succès", "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "Aucune source de contenu organisationnelle", - "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "Aucun utilisateur", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "Supprimer {name}", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "Votre groupe sera supprimé de Workplace Search. Voulez-vous vraiment supprimer {name} ?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "Confirmer", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2dfe1e52bcea..70b2f076d5f8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7478,7 +7478,6 @@ "xpack.apm.filter.environment.allLabel": "すべて", "xpack.apm.filter.environment.label": "環境", "xpack.apm.filter.environment.notDefinedLabel": "未定義", - "xpack.apm.filter.environment.selectEnvironmentLabel": "環境を選択", "xpack.apm.fleet_integration.settings.advancedOptionsLavel": "高度なオプション", "xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentHelpText": "匿名アクセスの許可されたエージェント名。", "xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentLabel": "許可されたエージェント", @@ -7703,7 +7702,6 @@ "xpack.apm.propertiesTable.tabs.metadataLabel": "メタデータ", "xpack.apm.propertiesTable.tabs.timelineLabel": "Timeline", "xpack.apm.searchInput.filter": "フィルター...", - "xpack.apm.selectCustomOptionText": "\\{searchValue\\}を新しいオプションとして追加", "xpack.apm.selectPlaceholder": "オプションを選択:", "xpack.apm.serviceDependencies.breakdownChartTitle": "依存関係にかかった時間", "xpack.apm.serviceDetails.dependenciesTabLabel": "依存関係", @@ -11923,7 +11921,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action": "グループを管理", "xpack.enterpriseSearch.workplaceSearch.groups.newGroupSavedSuccess": "{groupName}が正常に作成されました", "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "組織コンテンツソースがありません", - "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "ユーザーがありません", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "{name}を削除", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "グループはWorkplace Searchから削除されます。{name}を削除してよろしいですか?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "確認", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index de839f21af98..e249662ac511 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7496,7 +7496,6 @@ "xpack.apm.filter.environment.allLabel": "全部", "xpack.apm.filter.environment.label": "环境", "xpack.apm.filter.environment.notDefinedLabel": "未定义", - "xpack.apm.filter.environment.selectEnvironmentLabel": "选择环境", "xpack.apm.fleet_integration.settings.advancedOptionsLavel": "高级选项", "xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentHelpText": "允许进行匿名访问的代理名称。", "xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentLabel": "允许的代理", @@ -7723,7 +7722,6 @@ "xpack.apm.propertiesTable.tabs.metadataLabel": "元数据", "xpack.apm.propertiesTable.tabs.timelineLabel": "时间线", "xpack.apm.searchInput.filter": "筛选...", - "xpack.apm.selectCustomOptionText": "将 \\{searchValue\\} 添加为新选项", "xpack.apm.selectPlaceholder": "选择选项:", "xpack.apm.serviceDependencies.breakdownChartTitle": "依赖项花费的时间", "xpack.apm.serviceDetails.dependenciesTabLabel": "依赖项", @@ -11946,7 +11944,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action": "管理组", "xpack.enterpriseSearch.workplaceSearch.groups.newGroupSavedSuccess": "已成功创建 {groupName}", "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "无组织内容源", - "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "无用户", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "删除 {name}", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "您的组将从 Workplace Search 中删除。确定要移除 {name}?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "确认", diff --git a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts index 74bdc860bfba..692cd1c0cf7f 100644 --- a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts +++ b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts @@ -9,12 +9,14 @@ import { SERVICE_NAME, TRANSACTION_TYPE, } from '@kbn/apm-plugin/common/elasticsearch_fieldnames'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function suggestionsTests({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; + const { start, end } = archives_metadata[archiveName]; registry.when( 'suggestions when data is loaded', @@ -25,7 +27,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) { it('returns all environments', async () => { const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/suggestions', - params: { query: { field: SERVICE_ENVIRONMENT, string: '' } }, + params: { query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: '', start, end } }, }); expectSnapshot(body).toMatchInline(` @@ -43,7 +45,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) { it('returns items matching the string parameter', async () => { const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/suggestions', - params: { query: { field: SERVICE_ENVIRONMENT, string: 'pr' } }, + params: { query: { fieldName: SERVICE_ENVIRONMENT, fieldValue: 'pr', start, end } }, }); expectSnapshot(body).toMatchInline(` @@ -62,7 +64,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) { it('returns all services', async () => { const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/suggestions', - params: { query: { field: SERVICE_NAME, string: '' } }, + params: { query: { fieldName: SERVICE_NAME, fieldValue: '', start, end } }, }); expectSnapshot(body).toMatchInline(` @@ -86,7 +88,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) { it('returns items matching the string parameter', async () => { const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/suggestions', - params: { query: { field: SERVICE_NAME, string: 'aud' } }, + params: { query: { fieldName: SERVICE_NAME, fieldValue: 'aud', start, end } }, }); expectSnapshot(body).toMatchInline(` @@ -105,7 +107,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) { it('returns all transaction types', async () => { const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/suggestions', - params: { query: { field: TRANSACTION_TYPE, string: '' } }, + params: { query: { fieldName: TRANSACTION_TYPE, fieldValue: '', start, end } }, }); expectSnapshot(body).toMatchInline(` @@ -125,7 +127,7 @@ export default function suggestionsTests({ getService }: FtrProviderContext) { it('returns items matching the string parameter', async () => { const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/suggestions', - params: { query: { field: TRANSACTION_TYPE, string: 'w' } }, + params: { query: { fieldName: TRANSACTION_TYPE, fieldValue: 'w', start, end } }, }); expectSnapshot(body).toMatchInline(` diff --git a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts index 22b553d00630..5cba074eedbe 100644 --- a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts +++ b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts @@ -70,7 +70,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await retry.try(async () => { const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); - expect(apmMainContainerTextItems).to.not.contain('No services found'); expect(apmMainContainerTextItems).to.contain('opbeans-go'); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index 1497c85b91ba..d1a500be98ff 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -94,7 +94,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`allows a workpad to be edited`, async () => { await PageObjects.common.navigateToActualUrl( 'canvas', - 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', { ensureCurrentUrl: true, shouldLoginIfPrompted: false, @@ -171,7 +171,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`does not allow a workpad to be edited`, async () => { await PageObjects.common.navigateToActualUrl( 'canvas', - 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', { ensureCurrentUrl: true, shouldLoginIfPrompted: false, diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 5060ac60ecea..e030cdbf5f62 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -80,7 +80,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`allows a workpad to be edited`, async () => { await PageObjects.common.navigateToActualUrl( 'canvas', - 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', { ensureCurrentUrl: true, shouldLoginIfPrompted: false, diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index 053c58e88cde..dff3cb7ce30a 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -48,9 +48,9 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { await retry.try(async () => { const url = await browser.getCurrentUrl(); - // remove all the search params, just compare the route - const hashRoute = new URL(url).hash.split('?')[0]; - expect(hashRoute).to.equal(`#/workpad/${testWorkpadId}/page/1`); + const path = new URL(url).pathname; + + expect(path).to.equal(`/app/canvas/workpad/${testWorkpadId}/page/1`); }); }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 9751c7185213..d38264150cfa 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -655,7 +655,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async editDimensionLabel(label: string) { - await testSubjects.setValue('indexPattern-label-edit', label, { clearWithKeyboard: true }); + await testSubjects.setValue('column-label-edit', label, { clearWithKeyboard: true }); }, async editDimensionFormat(format: string) { const formatInput = await testSubjects.find('indexPattern-dimension-format'); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 368783b0efd1..48dc45073722 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -34,29 +34,29 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Last active', 'Actions', ], - ['Host-9fafsc3tqe', 'x', 'x', 'Warning', 'Windows', '10.231.117.28', '7.17.12', 'x', ''], [ 'Host-ku5jy6j0pw', 'x', 'x', - 'Warning', + 'Unsupported', 'Windows', - '10.246.87.11, 10.145.117.106,10.109.242.136', + '10.12.215.130, 10.130.188.228,10.19.102.141', '7.0.13', 'x', '', ], [ - 'Host-o07wj6uaa5', + 'Host-ntr4rkj24m', 'x', 'x', - 'Failure', + 'Success', 'Windows', - '10.82.134.220, 10.47.25.170', - '7.11.13', + '10.36.46.252, 10.222.152.110', + '7.4.13', 'x', '', ], + ['Host-q9qenwrl9k', 'x', 'x', 'Warning', 'Windows', '10.206.226.90', '7.11.10', 'x', ''], ]; const formattedTableData = async () => { @@ -209,9 +209,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Host-ku5jy6j0pw', 'x', 'x', - 'Warning', + 'Unsupported', 'Windows', - '10.246.87.11, 10.145.117.106,10.109.242.136', + '10.12.215.130, 10.130.188.228,10.19.102.141', '7.0.13', 'x', '', diff --git a/yarn.lock b/yarn.lock index afc8afb39df4..4ab577b3807d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16133,11 +16133,6 @@ highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@~10.4.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg== -history-extra@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/history-extra/-/history-extra-5.0.1.tgz#95a2e59dda526c4241d0ae1b124a77a5e4675ce8" - integrity sha512-6XV1L1lHgporVWgppa/Kq+Fnz4lhBew7iMxYCTfzVmoEywsAKJnTjdw1zOd+EGLHGYp0/V8jSVMEgqx4QbHLTw== - history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca"