From 79fefe0311ed5e22fde9d5dc85c7c922f21c1e3e Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Thu, 12 Aug 2021 17:11:53 +0200 Subject: [PATCH 01/91] [Security solution] [RAC] Add row renderer popover to alert table "reason" field (#108054) * Add row renderer popover to alert table reason field * Add a title to row renderer popover on alert table * Fix issues found during code review --- .../render_cell_value.tsx | 6 + .../row_renderers_browser/catalog/index.tsx | 52 ++++-- .../body/renderers/column_renderer.ts | 7 +- .../timeline/body/renderers/constants.tsx | 1 + .../timeline/body/renderers/index.ts | 2 + .../renderers/reason_column_renderer.test.tsx | 148 ++++++++++++++++ .../body/renderers/reason_column_renderer.tsx | 164 ++++++++++++++++++ .../timeline/body/renderers/translations.ts | 6 + .../cell_rendering/default_cell_renderer.tsx | 6 + .../common/types/timeline/cells/index.ts | 7 +- .../public/components/t_grid/body/index.tsx | 5 +- 11 files changed, 385 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.tsx diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx index 72914507bb6a6..46fb853a7aa29 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx @@ -31,6 +31,9 @@ export const RenderCellValue: React.FC< rowIndex, setCellProps, timelineId, + ecsData, + rowRenderers, + browserFields, }) => ( ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx index f724c19913c8e..548dadf21b78b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx @@ -40,6 +40,26 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) => ); +export const eventRendererNames: { [key in RowRendererId]: string } = { + [RowRendererId.alerts]: i18n.ALERTS_NAME, + [RowRendererId.auditd]: i18n.AUDITD_NAME, + [RowRendererId.auditd_file]: i18n.AUDITD_FILE_NAME, + [RowRendererId.library]: i18n.LIBRARY_NAME, + [RowRendererId.system_security_event]: i18n.AUTHENTICATION_NAME, + [RowRendererId.system_dns]: i18n.DNS_NAME, + [RowRendererId.netflow]: i18n.FLOW_NAME, + [RowRendererId.system]: i18n.SYSTEM_NAME, + [RowRendererId.system_endgame_process]: i18n.PROCESS, + [RowRendererId.registry]: i18n.REGISTRY_NAME, + [RowRendererId.system_fim]: i18n.FIM_NAME, + [RowRendererId.system_file]: i18n.FILE_NAME, + [RowRendererId.system_socket]: i18n.SOCKET_NAME, + [RowRendererId.suricata]: 'Suricata', + [RowRendererId.threat_match]: i18n.THREAT_MATCH_NAME, + [RowRendererId.zeek]: i18n.ZEEK_NAME, + [RowRendererId.plain]: '', +}; + export interface RowRendererOption { id: RowRendererId; name: string; @@ -51,14 +71,14 @@ export interface RowRendererOption { export const renderers: RowRendererOption[] = [ { id: RowRendererId.alerts, - name: i18n.ALERTS_NAME, + name: eventRendererNames[RowRendererId.alerts], description: i18n.ALERTS_DESCRIPTION, example: AlertsExample, searchableDescription: i18n.ALERTS_DESCRIPTION, }, { id: RowRendererId.auditd, - name: i18n.AUDITD_NAME, + name: eventRendererNames[RowRendererId.auditd], description: ( @@ -72,7 +92,7 @@ export const renderers: RowRendererOption[] = [ }, { id: RowRendererId.auditd_file, - name: i18n.AUDITD_FILE_NAME, + name: eventRendererNames[RowRendererId.auditd_file], description: ( @@ -86,14 +106,14 @@ export const renderers: RowRendererOption[] = [ }, { id: RowRendererId.library, - name: i18n.LIBRARY_NAME, + name: eventRendererNames[RowRendererId.library], description: i18n.LIBRARY_DESCRIPTION, example: LibraryExample, searchableDescription: i18n.LIBRARY_DESCRIPTION, }, { id: RowRendererId.system_security_event, - name: i18n.AUTHENTICATION_NAME, + name: eventRendererNames[RowRendererId.system_security_event], description: (

{i18n.AUTHENTICATION_DESCRIPTION_PART1}

@@ -106,14 +126,14 @@ export const renderers: RowRendererOption[] = [ }, { id: RowRendererId.system_dns, - name: i18n.DNS_NAME, + name: eventRendererNames[RowRendererId.system_dns], description: i18n.DNS_DESCRIPTION_PART1, example: SystemDnsExample, searchableDescription: i18n.DNS_DESCRIPTION_PART1, }, { id: RowRendererId.netflow, - name: i18n.FLOW_NAME, + name: eventRendererNames[RowRendererId.netflow], description: (

{i18n.FLOW_DESCRIPTION_PART1}

@@ -126,7 +146,7 @@ export const renderers: RowRendererOption[] = [ }, { id: RowRendererId.system, - name: i18n.SYSTEM_NAME, + name: eventRendererNames[RowRendererId.system], description: (

@@ -145,7 +165,7 @@ export const renderers: RowRendererOption[] = [ }, { id: RowRendererId.system_endgame_process, - name: i18n.PROCESS, + name: eventRendererNames[RowRendererId.system_endgame_process], description: (

{i18n.PROCESS_DESCRIPTION_PART1}

@@ -158,28 +178,28 @@ export const renderers: RowRendererOption[] = [ }, { id: RowRendererId.registry, - name: i18n.REGISTRY_NAME, + name: eventRendererNames[RowRendererId.registry], description: i18n.REGISTRY_DESCRIPTION, example: RegistryExample, searchableDescription: i18n.REGISTRY_DESCRIPTION, }, { id: RowRendererId.system_fim, - name: i18n.FIM_NAME, + name: eventRendererNames[RowRendererId.system_fim], description: i18n.FIM_DESCRIPTION_PART1, example: SystemFimExample, searchableDescription: i18n.FIM_DESCRIPTION_PART1, }, { id: RowRendererId.system_file, - name: i18n.FILE_NAME, + name: eventRendererNames[RowRendererId.system_file], description: i18n.FILE_DESCRIPTION_PART1, example: SystemFileExample, searchableDescription: i18n.FILE_DESCRIPTION_PART1, }, { id: RowRendererId.system_socket, - name: i18n.SOCKET_NAME, + name: eventRendererNames[RowRendererId.system_socket], description: (

{i18n.SOCKET_DESCRIPTION_PART1}

@@ -192,7 +212,7 @@ export const renderers: RowRendererOption[] = [ }, { id: RowRendererId.suricata, - name: 'Suricata', + name: eventRendererNames[RowRendererId.suricata], description: (

{i18n.SURICATA_DESCRIPTION_PART1}{' '} @@ -207,14 +227,14 @@ export const renderers: RowRendererOption[] = [ }, { id: RowRendererId.threat_match, - name: i18n.THREAT_MATCH_NAME, + name: eventRendererNames[RowRendererId.threat_match], description: i18n.THREAT_MATCH_DESCRIPTION, example: ThreatMatchExample, searchableDescription: `${i18n.THREAT_MATCH_NAME} ${i18n.THREAT_MATCH_DESCRIPTION}`, }, { id: RowRendererId.zeek, - name: i18n.ZEEK_NAME, + name: eventRendererNames[RowRendererId.zeek], description: (

{i18n.ZEEK_DESCRIPTION_PART1}{' '} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts index fc13680b81be2..1e6f613999ece 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts @@ -6,7 +6,9 @@ */ import type React from 'react'; -import { ColumnHeaderOptions } from '../../../../../../common'; + +import { BrowserFields, ColumnHeaderOptions, RowRenderer } from '../../../../../../common'; +import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; export interface ColumnRenderer { @@ -29,5 +31,8 @@ export interface ColumnRenderer { truncate?: boolean; values: string[] | null | undefined; linkValues?: string[] | null | undefined; + ecsData?: Ecs; + rowRenderers?: RowRenderer[]; + browserFields?: BrowserFields; }) => React.ReactNode; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx index aeb40bed26c8e..3a7a43da2aedc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx @@ -17,3 +17,4 @@ export const EVENT_URL_FIELD_NAME = 'event.url'; export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name'; export const SIGNAL_STATUS_FIELD_NAME = 'signal.status'; export const AGENT_STATUS_FIELD_NAME = 'agent.status'; +export const REASON_FIELD_NAME = 'signal.reason'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts index 911dcc8cd2e87..11c501f9426f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts @@ -16,6 +16,7 @@ import { unknownColumnRenderer } from './unknown_column_renderer'; import { zeekRowRenderer } from './zeek/zeek_row_renderer'; import { systemRowRenderers } from './system/generic_row_renderer'; import { threatMatchRowRenderer } from './cti/threat_match_row_renderer'; +import { reasonColumnRenderer } from './reason_column_renderer'; // The row renderers are order dependent and will return the first renderer // which returns true from its isInstance call. The bottom renderers which @@ -34,6 +35,7 @@ export const defaultRowRenderers: RowRenderer[] = [ ]; export const columnRenderers: ColumnRenderer[] = [ + reasonColumnRenderer, plainColumnRenderer, emptyColumnRenderer, unknownColumnRenderer, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx new file mode 100644 index 0000000000000..addb991af58d7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx @@ -0,0 +1,148 @@ +/* + * 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 { mockTimelineData } from '../../../../../common/mock'; +import { defaultColumnHeaderType } from '../column_headers/default_headers'; +import { REASON_FIELD_NAME } from './constants'; +import { reasonColumnRenderer } from './reason_column_renderer'; +import { plainColumnRenderer } from './plain_column_renderer'; + +import { + BrowserFields, + ColumnHeaderOptions, + RowRenderer, + RowRendererId, +} from '../../../../../../common'; +import { fireEvent, render } from '@testing-library/react'; +import { TestProviders } from '../../../../../../../timelines/public/mock'; +import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../../timelines/public/components'; +import { cloneDeep } from 'lodash'; +jest.mock('./plain_column_renderer'); + +jest.mock('../../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: () => ({ + services: { + timelines: { + getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper, + }, + }, + }), + }; +}); + +jest.mock('../../../../../common/components/link_to', () => { + const original = jest.requireActual('../../../../../common/components/link_to'); + return { + ...original, + useFormatUrl: () => ({ + formatUrl: () => '', + }), + }; +}); + +const invalidEcs = cloneDeep(mockTimelineData[0].ecs); +const validEcs = cloneDeep(mockTimelineData[28].ecs); + +const field: ColumnHeaderOptions = { + id: 'test-field-id', + columnHeaderType: defaultColumnHeaderType, +}; + +const rowRenderers: RowRenderer[] = [ + { + id: RowRendererId.alerts, + isInstance: (ecs) => ecs === validEcs, + // eslint-disable-next-line react/display-name + renderRow: () => , + }, +]; +const browserFields: BrowserFields = {}; + +const defaultProps = { + columnName: REASON_FIELD_NAME, + eventId: 'test-event-id', + field, + timelineId: 'test-timeline-id', + values: ['test-value'], +}; + +describe('reasonColumnRenderer', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('isIntance', () => { + it('returns true when columnName is `signal.reason`', () => { + expect(reasonColumnRenderer.isInstance(REASON_FIELD_NAME, [])).toBeTruthy(); + }); + }); + + describe('renderColumn', () => { + it('calls `plainColumnRenderer.renderColumn` when ecsData, rowRenderers or browserFields is empty', () => { + reasonColumnRenderer.renderColumn(defaultProps); + + expect(plainColumnRenderer.renderColumn).toBeCalledTimes(1); + }); + + it("doesn't call `plainColumnRenderer.renderColumn` when ecsData, rowRenderers or browserFields fields are not empty", () => { + reasonColumnRenderer.renderColumn({ + ...defaultProps, + ecsData: invalidEcs, + rowRenderers, + browserFields, + }); + + expect(plainColumnRenderer.renderColumn).toBeCalledTimes(0); + }); + + it("doesn't render popover button when getRowRenderer doesn't find a rowRenderer", () => { + const renderedColumn = reasonColumnRenderer.renderColumn({ + ...defaultProps, + ecsData: invalidEcs, + rowRenderers, + browserFields, + }); + + const wrapper = render({renderedColumn}); + + expect(wrapper.queryByTestId('reason-cell-button')).not.toBeInTheDocument(); + }); + + it('render popover button when getRowRenderer finds a rowRenderer', () => { + const renderedColumn = reasonColumnRenderer.renderColumn({ + ...defaultProps, + ecsData: validEcs, + rowRenderers, + browserFields, + }); + + const wrapper = render({renderedColumn}); + + expect(wrapper.queryByTestId('reason-cell-button')).toBeInTheDocument(); + }); + + it('render rowRender inside a popover when reson field button is clicked', () => { + const renderedColumn = reasonColumnRenderer.renderColumn({ + ...defaultProps, + ecsData: validEcs, + rowRenderers, + browserFields, + }); + + const wrapper = render({renderedColumn}); + + fireEvent.click(wrapper.getByTestId('reason-cell-button')); + + expect(wrapper.queryByTestId('test-row-render')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.tsx new file mode 100644 index 0000000000000..0914c861d00ed --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.tsx @@ -0,0 +1,164 @@ +/* + * 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 { EuiButtonEmpty, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { useCallback, useMemo, useState } from 'react'; + +import styled from 'styled-components'; +import { BrowserFields, ColumnHeaderOptions, RowRenderer } from '../../../../../../common'; +import { Ecs } from '../../../../../../common/ecs'; +import { DefaultDraggable } from '../../../../../common/components/draggables'; +import { eventRendererNames } from '../../../row_renderers_browser/catalog'; +import { ColumnRenderer } from './column_renderer'; +import { REASON_FIELD_NAME } from './constants'; +import { getRowRenderer } from './get_row_renderer'; +import { plainColumnRenderer } from './plain_column_renderer'; +import * as i18n from './translations'; + +export const reasonColumnRenderer: ColumnRenderer = { + isInstance: isEqual(REASON_FIELD_NAME), + + renderColumn: ({ + columnName, + eventId, + field, + isDraggable = true, + timelineId, + truncate, + values, + linkValues, + ecsData, + rowRenderers = [], + browserFields, + }: { + columnName: string; + eventId: string; + field: ColumnHeaderOptions; + isDraggable?: boolean; + timelineId: string; + truncate?: boolean; + values: string[] | undefined | null; + linkValues?: string[] | null | undefined; + + ecsData?: Ecs; + rowRenderers?: RowRenderer[]; + browserFields?: BrowserFields; + }) => + values != null && ecsData && rowRenderers?.length > 0 && browserFields + ? values.map((value, i) => ( + + )) + : plainColumnRenderer.renderColumn({ + columnName, + eventId, + field, + isDraggable, + timelineId, + truncate, + values, + linkValues, + }), +}; + +const StyledEuiButtonEmpty = styled(EuiButtonEmpty)` + font-weight: ${(props) => props.theme.eui.euiFontWeightRegular}; +`; + +const ReasonCell: React.FC<{ + contextId: string; + eventId: string; + fieldName: string; + isDraggable?: boolean; + value: string | number | undefined | null; + timelineId: string; + ecsData: Ecs; + rowRenderers: RowRenderer[]; + browserFields: BrowserFields; +}> = ({ + ecsData, + rowRenderers, + browserFields, + timelineId, + value, + fieldName, + isDraggable, + contextId, + eventId, +}) => { + const [isOpen, setIsOpen] = useState(false); + + const rowRenderer = useMemo(() => getRowRenderer(ecsData, rowRenderers), [ecsData, rowRenderers]); + + const rowRender = useMemo(() => { + return ( + rowRenderer && + rowRenderer.renderRow({ + browserFields, + data: ecsData, + isDraggable: true, + timelineId, + }) + ); + }, [rowRenderer, browserFields, ecsData, timelineId]); + + const handleTogglePopOver = useCallback(() => setIsOpen(!isOpen), [setIsOpen, isOpen]); + const handleClosePopOver = useCallback(() => setIsOpen(false), [setIsOpen]); + + const button = useMemo( + () => ( + + {value} + + ), + [value, handleTogglePopOver] + ); + + return ( + <> + + {rowRenderer && rowRender ? ( + + + {i18n.EVENT_RENDERER_POPOVER_TITLE(eventRendererNames[rowRenderer.id] ?? '')} + + {rowRender} + + ) : ( + value + )} + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/translations.ts index d00148d41f3f6..a703c7afdaf7b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/translations.ts @@ -51,3 +51,9 @@ export const EMPTY_STATUS = i18n.translate( defaultMessage: '-', } ); + +export const EVENT_RENDERER_POPOVER_TITLE = (eventRendererName: string) => + i18n.translate('xpack.securitySolution.event.reason.eventRenderPopoverTitle', { + values: { eventRendererName }, + defaultMessage: 'Event renderer: {eventRendererName} ', + }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx index d2652ed063fc7..d45c8103d1cca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -22,6 +22,9 @@ export const DefaultCellRenderer: React.FC = ({ linkValues, setCellProps, timelineId, + rowRenderers, + browserFields, + ecsData, }) => ( <> {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ @@ -36,6 +39,9 @@ export const DefaultCellRenderer: React.FC = ({ data, fieldName: header.id, }), + rowRenderers, + browserFields, + ecsData, })} ); diff --git a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts index 2a6e1b3e12bcf..354a8b45e914d 100644 --- a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts @@ -6,7 +6,9 @@ */ import { EuiDataGridCellValueElementProps } from '@elastic/eui'; -import { TimelineNonEcsData } from '../../../search_strategy'; +import { RowRenderer } from '../../..'; +import { Ecs } from '../../../ecs'; +import { BrowserFields, TimelineNonEcsData } from '../../../search_strategy'; import { ColumnHeaderOptions } from '../columns'; /** The following props are provided to the function called by `renderCellValue` */ @@ -19,4 +21,7 @@ export type CellValueElementProps = EuiDataGridCellValueElementProps & { timelineId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any setFlyoutAlert?: (data: any) => void; + ecsData?: Ecs; + rowRenderers?: RowRenderer[]; + browserFields?: BrowserFields; }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index cc94f901446a7..5fba7cff55e5c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -526,9 +526,12 @@ export const BodyComponent = React.memo( rowIndex, setCellProps, timelineId: tabType != null ? `${id}-${tabType}` : id, + ecsData: data[rowIndex].ecs, + browserFields, + rowRenderers, }); }, - [columnHeaders, data, id, renderCellValue, tabType, theme] + [columnHeaders, data, id, renderCellValue, tabType, theme, browserFields, rowRenderers] ); return ( From 4505f3ba4f3cc7366d8c389883676c06e6309870 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 12 Aug 2021 17:53:22 +0200 Subject: [PATCH 02/91] [APM] Show relevant nodes in focused service map (#108028) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/service_map/get_service_map.ts | 2 - .../get_service_map_from_trace_ids.test.ts | 124 +++--------------- .../get_service_map_from_trace_ids.ts | 45 ------- .../lib/service_map/get_trace_sample_ids.ts | 25 ++-- 4 files changed, 29 insertions(+), 167 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 5fc022508d0a8..d7c210da8b999 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -67,8 +67,6 @@ async function getConnectionData({ chunks.map((traceIdsChunk) => getServiceMapFromTraceIds({ setup, - serviceName, - environment, traceIds: traceIdsChunk, }) ) diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.test.ts index 1e3f9df39aef9..b6c9b90bc5d22 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.test.ts @@ -7,7 +7,6 @@ import { getConnections } from './get_service_map_from_trace_ids'; import { Connection, ConnectionNode } from '../../../common/service_map'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; function getConnectionsPairs(connections: Connection[]) { return connections @@ -69,119 +68,26 @@ describe('getConnections', () => { }, ], ] as ConnectionNode[][]; - describe('if neither service name or environment is given', () => { - it('includes all connections', () => { - const connections = getConnections({ - paths, - serviceName: undefined, - environment: undefined, - }); - const connectionsPairs = getConnectionsPairs(connections); - expect(connectionsPairs).toEqual([ - 'opbeans-ruby:testing -> opbeans-node:null', - 'opbeans-node:null -> opbeans-go:production', - 'opbeans-go:production -> opbeans-java:production', - 'opbeans-java:production -> external', - 'opbeans-ruby:testing -> opbeans-python:testing', - 'opbeans-python:testing -> external', - ]); - }); - }); - - describe('if service name and environment are given', () => { - it('shows all connections for opbeans-java and production', () => { - const connections = getConnections({ - paths, - serviceName: 'opbeans-java', - environment: 'production', - }); - - const connectionsPairs = getConnectionsPairs(connections); - - expect(connectionsPairs).toEqual([ - 'opbeans-ruby:testing -> opbeans-node:null', - 'opbeans-node:null -> opbeans-go:production', - 'opbeans-go:production -> opbeans-java:production', - 'opbeans-java:production -> external', - ]); - }); - - it('shows all connections for opbeans-python and testing', () => { - const connections = getConnections({ - paths, - serviceName: 'opbeans-python', - environment: 'testing', - }); - - const connectionsPairs = getConnectionsPairs(connections); - - expect(connectionsPairs).toEqual([ - 'opbeans-ruby:testing -> opbeans-python:testing', - 'opbeans-python:testing -> external', - ]); - }); - }); - - describe('if service name is given', () => { - it('shows all connections for opbeans-node', () => { - const connections = getConnections({ - paths, - serviceName: 'opbeans-node', - environment: undefined, - }); - - const connectionsPairs = getConnectionsPairs(connections); - - expect(connectionsPairs).toEqual([ - 'opbeans-ruby:testing -> opbeans-node:null', - 'opbeans-node:null -> opbeans-go:production', - 'opbeans-go:production -> opbeans-java:production', - 'opbeans-java:production -> external', - ]); - }); - }); - - describe('if environment is given', () => { - it('shows all connections for testing environment', () => { - const connections = getConnections({ - paths, - serviceName: undefined, - environment: 'testing', - }); - - const connectionsPairs = getConnectionsPairs(connections); - - expect(connectionsPairs).toEqual([ - 'opbeans-ruby:testing -> opbeans-node:null', - 'opbeans-node:null -> opbeans-go:production', - 'opbeans-go:production -> opbeans-java:production', - 'opbeans-java:production -> external', - 'opbeans-ruby:testing -> opbeans-python:testing', - 'opbeans-python:testing -> external', - ]); + it('includes all connections', () => { + const connections = getConnections({ + paths, }); - it('shows all connections for production environment', () => { - const connections = getConnections({ - paths, - serviceName: undefined, - environment: 'production', - }); - - const connectionsPairs = getConnectionsPairs(connections); - expect(connectionsPairs).toEqual([ - 'opbeans-ruby:testing -> opbeans-node:null', - 'opbeans-node:null -> opbeans-go:production', - 'opbeans-go:production -> opbeans-java:production', - 'opbeans-java:production -> external', - ]); - }); + const connectionsPairs = getConnectionsPairs(connections); + expect(connectionsPairs).toEqual([ + 'opbeans-ruby:testing -> opbeans-node:null', + 'opbeans-node:null -> opbeans-go:production', + 'opbeans-go:production -> opbeans-java:production', + 'opbeans-java:production -> external', + 'opbeans-ruby:testing -> opbeans-python:testing', + 'opbeans-python:testing -> external', + ]); }); }); describe('environment is "not defined"', () => { - it('shows all connections where environment is not set', () => { + it('includes all connections', () => { const environmentNotDefinedPaths = [ [ { @@ -237,12 +143,12 @@ describe('getConnections', () => { ] as ConnectionNode[][]; const connections = getConnections({ paths: environmentNotDefinedPaths, - serviceName: undefined, - environment: ENVIRONMENT_NOT_DEFINED.value, }); const connectionsPairs = getConnectionsPairs(connections); expect(connectionsPairs).toEqual([ + 'opbeans-go:production -> opbeans-java:production', + 'opbeans-java:production -> external', 'opbeans-go:null -> opbeans-java:null', 'opbeans-java:null -> external', 'opbeans-python:null -> opbeans-node:null', diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts index 7e43c8f684377..ed99f1909ab07 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts @@ -6,58 +6,19 @@ */ import { find, uniqBy } from 'lodash'; -import { - ENVIRONMENT_ALL, - ENVIRONMENT_NOT_DEFINED, -} from '../../../common/environment_filter_values'; -import { - SERVICE_ENVIRONMENT, - SERVICE_NAME, -} from '../../../common/elasticsearch_fieldnames'; import { Connection, ConnectionNode } from '../../../common/service_map'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { fetchServicePathsFromTraceIds } from './fetch_service_paths_from_trace_ids'; export function getConnections({ paths, - serviceName, - environment, }: { paths: ConnectionNode[][] | undefined; - serviceName: string | undefined; - environment: string | undefined; }) { if (!paths) { return []; } - if (serviceName || environment) { - paths = paths.filter((path) => { - return ( - path - // Only apply the filter on node that contains service name, this filters out external nodes - .filter((node) => { - return node[SERVICE_NAME]; - }) - .some((node) => { - if (serviceName && node[SERVICE_NAME] !== serviceName) { - return false; - } - - if (!environment || environment === ENVIRONMENT_ALL.value) { - return true; - } - - if (environment === ENVIRONMENT_NOT_DEFINED.value) { - return !node[SERVICE_ENVIRONMENT]; - } - - return node[SERVICE_ENVIRONMENT] === environment; - }) - ); - }); - } - const connectionsArr = paths.flatMap((path) => { return path.reduce((conns, location, index) => { const prev = path[index - 1]; @@ -81,13 +42,9 @@ export function getConnections({ export async function getServiceMapFromTraceIds({ setup, traceIds, - serviceName, - environment, }: { setup: Setup & SetupTimeRange; traceIds: string[]; - serviceName?: string; - environment?: string; }) { const serviceMapFromTraceIdsScriptResponse = await fetchServicePathsFromTraceIds( setup, @@ -100,8 +57,6 @@ export async function getServiceMapFromTraceIds({ return { connections: getConnections({ paths: serviceMapScriptedAggValue?.paths, - serviceName, - environment, }), discoveredServices: serviceMapScriptedAggValue?.discoveredServices ?? [], }; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index c97bfc1cfacc8..5845ba6f5f1a5 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -8,7 +8,6 @@ import Boom from '@hapi/boom'; import { sortBy, take, uniq } from 'lodash'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; -import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, @@ -36,19 +35,22 @@ export async function getTraceSampleIds({ const query = { bool: { - filter: [ - { - exists: { - field: SPAN_DESTINATION_SERVICE_RESOURCE, - }, - }, - ...rangeQuery(start, end), - ] as ESFilter[], + filter: [...rangeQuery(start, end)], }, - } as { bool: { filter: ESFilter[]; must_not?: ESFilter[] | ESFilter } }; + }; + + let events: ProcessorEvent[]; if (serviceName) { query.bool.filter.push({ term: { [SERVICE_NAME]: serviceName } }); + events = [ProcessorEvent.span, ProcessorEvent.transaction]; + } else { + events = [ProcessorEvent.span]; + query.bool.filter.push({ + exists: { + field: SPAN_DESTINATION_SERVICE_RESOURCE, + }, + }); } query.bool.filter.push(...environmentQuery(environment)); @@ -65,7 +67,7 @@ export async function getTraceSampleIds({ const params = { apm: { - events: [ProcessorEvent.span], + events, }, body: { size: 0, @@ -78,6 +80,7 @@ export async function getTraceSampleIds({ [SPAN_DESTINATION_SERVICE_RESOURCE]: { terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE, + missing_bucket: true, }, }, }, From 75db4e56a6a7f5ca78b9ff05f6af3e208f2f1f9d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 12 Aug 2021 17:54:27 +0200 Subject: [PATCH 03/91] [APM] Merge environments from metric-only services (#108185) --- .../get_services/get_services_items.ts | 34 +--- .../get_services/merge_service_stats.test.ts | 183 ++++++++++++++++++ .../get_services/merge_service_stats.ts | 62 ++++++ 3 files changed, 251 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/services/get_services/merge_service_stats.test.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_services/merge_service_stats.ts diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 3b9792519d261..5c7579c779eaf 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -6,13 +6,12 @@ */ import { Logger } from '@kbn/logging'; -import { asMutableArray } from '../../../../common/utils/as_mutable_array'; -import { joinByKey } from '../../../../common/utils/join_by_key'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getHealthStatuses } from './get_health_statuses'; import { getServicesFromMetricDocuments } from './get_services_from_metric_documents'; import { getServiceTransactionStats } from './get_service_transaction_stats'; +import { mergeServiceStats } from './merge_service_stats'; export type ServicesItemsSetup = Setup & SetupTimeRange; @@ -53,31 +52,10 @@ export async function getServicesItems({ }), ]); - const foundServiceNames = transactionStats.map( - ({ serviceName }) => serviceName - ); - - const servicesWithOnlyMetricDocuments = servicesFromMetricDocuments.filter( - ({ serviceName }) => !foundServiceNames.includes(serviceName) - ); - - const allServiceNames = foundServiceNames.concat( - servicesWithOnlyMetricDocuments.map(({ serviceName }) => serviceName) - ); - - // make sure to exclude health statuses from services - // that are not found in APM data - const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => - allServiceNames.includes(serviceName) - ); - - return joinByKey( - asMutableArray([ - ...transactionStats, - ...servicesWithOnlyMetricDocuments, - ...matchedHealthStatuses, - ] as const), - 'serviceName' - ); + return mergeServiceStats({ + transactionStats, + servicesFromMetricDocuments, + healthStatuses, + }); }); } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/merge_service_stats.test.ts b/x-pack/plugins/apm/server/lib/services/get_services/merge_service_stats.test.ts new file mode 100644 index 0000000000000..9fa077ee67c46 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/merge_service_stats.test.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ServiceHealthStatus } from '../../../../common/service_health_status'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { getServiceTransactionStats } from './get_service_transaction_stats'; +import { mergeServiceStats } from './merge_service_stats'; + +type ServiceTransactionStat = PromiseReturnType< + typeof getServiceTransactionStats +>[number]; + +function stat(values: Partial): ServiceTransactionStat { + return { + serviceName: 'opbeans-java', + environments: ['production'], + latency: 1, + throughput: 2, + transactionErrorRate: 3, + transactionType: 'request', + agentName: 'java', + ...values, + }; +} + +describe('mergeServiceStats', () => { + it('joins stats by service name', () => { + expect( + mergeServiceStats({ + transactionStats: [ + stat({ + serviceName: 'opbeans-java', + environments: ['production'], + }), + stat({ + serviceName: 'opbeans-java-2', + environments: ['staging'], + throughput: 4, + }), + ], + servicesFromMetricDocuments: [ + { + environments: ['production'], + serviceName: 'opbeans-java', + agentName: 'java', + }, + ], + healthStatuses: [ + { + healthStatus: ServiceHealthStatus.healthy, + serviceName: 'opbeans-java', + }, + ], + }) + ).toEqual([ + { + agentName: 'java', + environments: ['staging'], + serviceName: 'opbeans-java-2', + latency: 1, + throughput: 4, + transactionErrorRate: 3, + transactionType: 'request', + }, + { + agentName: 'java', + environments: ['production'], + healthStatus: ServiceHealthStatus.healthy, + serviceName: 'opbeans-java', + latency: 1, + throughput: 2, + transactionErrorRate: 3, + transactionType: 'request', + }, + ]); + }); + + it('shows services that only have metric documents', () => { + expect( + mergeServiceStats({ + transactionStats: [ + stat({ + serviceName: 'opbeans-java-2', + environments: ['staging'], + }), + ], + servicesFromMetricDocuments: [ + { + environments: ['production'], + serviceName: 'opbeans-java', + agentName: 'java', + }, + ], + healthStatuses: [ + { + healthStatus: ServiceHealthStatus.healthy, + serviceName: 'opbeans-java', + }, + ], + }) + ).toEqual([ + { + agentName: 'java', + environments: ['staging'], + serviceName: 'opbeans-java-2', + latency: 1, + throughput: 2, + transactionErrorRate: 3, + transactionType: 'request', + }, + { + agentName: 'java', + environments: ['production'], + healthStatus: ServiceHealthStatus.healthy, + serviceName: 'opbeans-java', + }, + ]); + }); + + it('does not show services that only have ML data', () => { + expect( + mergeServiceStats({ + transactionStats: [ + stat({ + serviceName: 'opbeans-java-2', + environments: ['staging'], + }), + ], + servicesFromMetricDocuments: [], + healthStatuses: [ + { + healthStatus: ServiceHealthStatus.healthy, + serviceName: 'opbeans-java', + }, + ], + }) + ).toEqual([ + { + agentName: 'java', + environments: ['staging'], + serviceName: 'opbeans-java-2', + latency: 1, + throughput: 2, + transactionErrorRate: 3, + transactionType: 'request', + }, + ]); + }); + + it('concatenates environments from metric/transaction data', () => { + expect( + mergeServiceStats({ + transactionStats: [ + stat({ + serviceName: 'opbeans-java', + environments: ['staging'], + }), + ], + servicesFromMetricDocuments: [ + { + environments: ['production'], + serviceName: 'opbeans-java', + agentName: 'java', + }, + ], + healthStatuses: [], + }) + ).toEqual([ + { + agentName: 'java', + environments: ['staging', 'production'], + serviceName: 'opbeans-java', + latency: 1, + throughput: 2, + transactionErrorRate: 3, + transactionType: 'request', + }, + ]); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/services/get_services/merge_service_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/merge_service_stats.ts new file mode 100644 index 0000000000000..00aaa6fd7066d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/merge_service_stats.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { uniq } from 'lodash'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; +import { joinByKey } from '../../../../common/utils/join_by_key'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { getHealthStatuses } from './get_health_statuses'; +import { getServicesFromMetricDocuments } from './get_services_from_metric_documents'; +import { getServiceTransactionStats } from './get_service_transaction_stats'; + +export function mergeServiceStats({ + transactionStats, + servicesFromMetricDocuments, + healthStatuses, +}: { + transactionStats: PromiseReturnType; + servicesFromMetricDocuments: PromiseReturnType< + typeof getServicesFromMetricDocuments + >; + healthStatuses: PromiseReturnType; +}) { + const foundServiceNames = transactionStats.map( + ({ serviceName }) => serviceName + ); + + const servicesWithOnlyMetricDocuments = servicesFromMetricDocuments.filter( + ({ serviceName }) => !foundServiceNames.includes(serviceName) + ); + + const allServiceNames = foundServiceNames.concat( + servicesWithOnlyMetricDocuments.map(({ serviceName }) => serviceName) + ); + + // make sure to exclude health statuses from services + // that are not found in APM data + const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => + allServiceNames.includes(serviceName) + ); + + return joinByKey( + asMutableArray([ + ...transactionStats, + ...servicesFromMetricDocuments, + ...matchedHealthStatuses, + ] as const), + 'serviceName', + function merge(a, b) { + const aEnvs = 'environments' in a ? a.environments : []; + const bEnvs = 'environments' in b ? b.environments : []; + + return { + ...a, + ...b, + environments: uniq(aEnvs.concat(bEnvs)), + }; + } + ); +} From 7f19e57ee5fdd195189dd8650b52e29975b02cfb Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 12 Aug 2021 12:04:41 -0400 Subject: [PATCH 04/91] Enabling a11y test (#108284) --- x-pack/test/accessibility/apps/spaces.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index b85364bb84766..daddb9b24fe03 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -5,7 +5,7 @@ * 2.0. */ -// a11y tests for spaces, space selection and spacce creation and feature controls +// a11y tests for spaces, space selection and space creation and feature controls import { FtrProviderContext } from '../ftr_provider_context'; @@ -52,7 +52,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // EUI issue - https://github.com/elastic/eui/issues/3999 - it.skip('a11y test for color picker', async () => { + it('a11y test for color picker', async () => { await PageObjects.spaceSelector.clickColorPicker(); await a11y.testAppSnapshot(); await browser.pressKeys(browser.keys.ESCAPE); From 40842e1d654c7ba7e71a0486ca76ae8f647fc007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 12 Aug 2021 12:16:09 -0400 Subject: [PATCH 05/91] [APM] Removing loading spinner while fetching comparison data from tables (#108256) * removing loading from tables * renaiming readme file * changing apm readme file * addressing PR comments --- x-pack/plugins/apm/ftr_e2e/README.md | 26 +++++++++++++++++++ .../integration/read_only_user/home.spec.ts | 19 +++++++------- .../service_overview/instances_table.spec.ts | 6 ++--- x-pack/plugins/apm/ftr_e2e/cypress_run.ts | 6 +++-- x-pack/plugins/apm/ftr_e2e/cypress_start.ts | 22 +++++++++------- .../app/service_inventory/index.tsx | 20 +++++++------- .../service_overview_errors_table/index.tsx | 6 +---- ...ice_overview_instances_chart_and_table.tsx | 6 +---- .../shared/transactions_table/index.tsx | 9 ++----- x-pack/plugins/apm/readme.md | 19 ++++++-------- .../apm/scripts/ftr_e2e/cypress_run.js | 5 +++- 11 files changed, 79 insertions(+), 65 deletions(-) create mode 100644 x-pack/plugins/apm/ftr_e2e/README.md diff --git a/x-pack/plugins/apm/ftr_e2e/README.md b/x-pack/plugins/apm/ftr_e2e/README.md new file mode 100644 index 0000000000000..007200a6a549a --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/README.md @@ -0,0 +1,26 @@ +# APM E2E + +APM uses [FTR](../../../../packages/kbn-test/README.md) (functional test runner) and [Cypress](https://www.cypress.io/) to run the e2e tests. The tests are located at `kibana/x-pack/plugins/apm/ftr_e2e/cypress/integration`. + +## Running tests + +**Run all tests** + +```sh +//kibana directory +node x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js +``` + +**Run specific test** + +```sh +//kibana directory +node x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js --spec ./cypress/integration/read_only_user/home.spec.ts +``` + +## Opening tests + +```sh +//kibana directory +node x-pack/plugins/apm/scripts/ftr_e2e/cypress_open.js +``` diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index 76461d49ba012..e7fe80a60e57a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -7,6 +7,7 @@ import url from 'url'; import archives_metadata from '../../fixtures/es_archiver/archives_metadata'; +import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; @@ -27,6 +28,12 @@ const apisToIntercept = [ ]; describe('Home page', () => { + before(() => { + esArchiverLoad('apm_8.0.0'); + }); + after(() => { + esArchiverUnload('apm_8.0.0'); + }); beforeEach(() => { cy.loginAsReadOnlyUser(); }); @@ -39,8 +46,7 @@ describe('Home page', () => { ); }); - // Flaky - it.skip('includes services with only metric documents', () => { + it('includes services with only metric documents', () => { cy.visit( `${serviceInventoryHref}&kuery=not%2520(processor.event%2520%253A%2522transaction%2522%2520)` ); @@ -50,11 +56,7 @@ describe('Home page', () => { }); describe('navigations', () => { - /* - This test is flaky, there's a problem with EuiBasicTable, that it blocks any action while loading is enabled. - So it might fail to click on the service link. - */ - it.skip('navigates to service overview page with transaction type', () => { + it('navigates to service overview page with transaction type', () => { apisToIntercept.map(({ endpoint, name }) => { cy.intercept('GET', endpoint).as(name); }); @@ -63,9 +65,6 @@ describe('Home page', () => { cy.contains('Services'); - cy.wait('@servicesMainStatistics', { responseTimeout: 10000 }); - cy.wait('@servicesDetailedStatistics', { responseTimeout: 10000 }); - cy.get('[data-test-subj="serviceLink_rum-js"]').then((element) => { element[0].click(); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts index 40a08035f5213..9428f9b9e6bb6 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts @@ -67,8 +67,7 @@ describe('Instances table', () => { cy.contains('opbeans-java'); cy.contains(serviceNodeName); }); - // For some reason the details panel is not opening after clicking on the button. - it.skip('shows instance details', () => { + it('shows instance details', () => { apisToIntercept.map(({ endpoint, name }) => { cy.intercept('GET', endpoint).as(name); }); @@ -88,8 +87,7 @@ describe('Instances table', () => { cy.contains('Service'); }); }); - // For some reason the tooltip is not opening after clicking on the button. - it.skip('shows actions available', () => { + it('shows actions available', () => { apisToIntercept.map(({ endpoint, name }) => { cy.intercept('GET', endpoint).as(name); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_run.ts b/x-pack/plugins/apm/ftr_e2e/cypress_run.ts index 0e9efb775fc7a..eb319f4b30835 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_run.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_run.ts @@ -4,15 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { argv } from 'yargs'; import { FtrConfigProviderContext } from '@kbn/test'; import { cypressRunTests } from './cypress_start'; +const spec = argv.grep as string; + async function runE2ETests({ readConfigFile }: FtrConfigProviderContext) { const cypressConfig = await readConfigFile(require.resolve('./config.ts')); return { ...cypressConfig.getAll(), - testRunner: cypressRunTests, + testRunner: cypressRunTests(spec), }; } diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts index a6027367d7868..7468a4473b311 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts @@ -11,17 +11,19 @@ import { FtrProviderContext } from './ftr_provider_context'; import archives_metadata from './cypress/fixtures/es_archiver/archives_metadata'; import { createKibanaUserRole } from '../scripts/kibana-security/create_kibana_user_role'; -export async function cypressRunTests({ getService }: FtrProviderContext) { - try { - const result = await cypressStart(getService, cypress.run); +export function cypressRunTests(spec?: string) { + return async ({ getService }: FtrProviderContext) => { + try { + const result = await cypressStart(getService, cypress.run, spec); - if (result && (result.status === 'failed' || result.totalFailed > 0)) { + if (result && (result.status === 'failed' || result.totalFailed > 0)) { + process.exit(1); + } + } catch (error) { + console.error('errors: ', error); process.exit(1); } - } catch (error) { - console.error('errors: ', error); - process.exit(1); - } + }; } export async function cypressOpenTests({ getService }: FtrProviderContext) { @@ -30,7 +32,8 @@ export async function cypressOpenTests({ getService }: FtrProviderContext) { async function cypressStart( getService: FtrProviderContext['getService'], - cypressExecution: typeof cypress.run | typeof cypress.open + cypressExecution: typeof cypress.run | typeof cypress.open, + spec?: string ) { const config = getService('config'); @@ -56,6 +59,7 @@ async function cypressStart( }); return cypressExecution({ + ...(spec !== 'undefined' ? { spec } : {}), config: { baseUrl: kibanaUrl }, env: { START_DATE: start, diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 2a59a98ee31f7..b807510106f01 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -82,7 +82,7 @@ function useServicesFetcher() { const { mainStatisticsData, requestId } = data; - const { data: comparisonData, status: comparisonStatus } = useFetcher( + const { data: comparisonData } = useFetcher( (callApmApi) => { if (start && end && mainStatisticsData.items.length) { return callApmApi({ @@ -146,12 +146,10 @@ function useServicesFetcher() { ]); return { - servicesData: mainStatisticsData, - servicesStatus: mainStatisticsStatus, + mainStatisticsData, + mainStatisticsStatus, comparisonData, - isLoading: - mainStatisticsStatus === FETCH_STATUS.LOADING || - comparisonStatus === FETCH_STATUS.LOADING, + isLoading: mainStatisticsStatus === FETCH_STATUS.LOADING, }; } @@ -159,8 +157,8 @@ export function ServiceInventory() { const { core } = useApmPluginContext(); const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { - servicesData, - servicesStatus, + mainStatisticsData, + mainStatisticsStatus, comparisonData, isLoading, } = useServicesFetcher(); @@ -200,13 +198,13 @@ export function ServiceInventory() { ) } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index c8604be50ee15..2aad045406f9f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -143,7 +143,6 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { data: errorGroupDetailedStatistics = INITIAL_STATE_DETAILED_STATISTICS, - status: errorGroupDetailedStatisticsStatus, } = useFetcher( (callApmApi) => { if (requestId && items.length && start && end && transactionType) { @@ -220,10 +219,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { pageSizeOptions: [PAGE_SIZE], hidePerPageOptions: true, }} - loading={ - status === FETCH_STATUS.LOADING || - errorGroupDetailedStatisticsStatus === FETCH_STATUS.LOADING - } + loading={status === FETCH_STATUS.LOADING} onChange={(newTableOptions: { page?: { index: number; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index a6ef16fe85510..7723e55043e42 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -166,7 +166,6 @@ export function ServiceOverviewInstancesChartAndTable({ const { data: detailedStatsData = INITIAL_STATE_DETAILED_STATISTICS, - status: detailedStatsStatus, } = useFetcher( (callApmApi) => { if ( @@ -228,10 +227,7 @@ export function ServiceOverviewInstancesChartAndTable({ detailedStatsData={detailedStatsData} serviceName={serviceName} tableOptions={tableOptions} - isLoading={ - mainStatsStatus === FETCH_STATUS.LOADING || - detailedStatsStatus === FETCH_STATUS.LOADING - } + isLoading={mainStatsStatus === FETCH_STATUS.LOADING} onChangeTableOptions={(newTableOptions) => { setTableOptions({ pageIndex: newTableOptions.page?.index ?? 0, diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 7f1dc2cc150d7..945f259d6d845 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -169,10 +169,7 @@ export function TransactionsTable({ }, } = data; - const { - data: transactionGroupDetailedStatistics, - status: transactionGroupDetailedStatisticsStatus, - } = useFetcher( + const { data: transactionGroupDetailedStatistics } = useFetcher( (callApmApi) => { if ( transactionGroupsTotalItems && @@ -217,9 +214,7 @@ export function TransactionsTable({ comparisonEnabled, }); - const isLoading = - status === FETCH_STATUS.LOADING || - transactionGroupDetailedStatisticsStatus === FETCH_STATUS.LOADING; + const isLoading = status === FETCH_STATUS.LOADING; const pagination = { pageIndex, diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index 9cfb6210e2541..ef3e1f018ded6 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -33,11 +33,7 @@ _Docker Compose is required_ ### Cypress tests -```sh -node x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js -``` - -_Starts Kibana (:5701), APM Server (:8201) and Elasticsearch (:9201). Ingests sample data into Elasticsearch via APM Server and runs the Cypress tests_ +See [ftr_e2e](./ftr_e2e) ### Jest tests @@ -82,9 +78,10 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) ### API integration tests -API tests are separated in two suites: - - a basic license test suite - - a trial license test suite (the equivalent of gold+) +API tests are separated in two suites: + +- a basic license test suite +- a trial license test suite (the equivalent of gold+) This requires separate test servers and test runners. @@ -112,10 +109,10 @@ node scripts/functional_test_runner --config x-pack/test/apm_api_integration/tri The API tests for "trial" are located in `x-pack/test/apm_api_integration/trial/tests`. - **API Test tips** - - For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) - - To update snapshots append `--updateSnapshots` to the functional_test_runner command + +- For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) +- To update snapshots append `--updateSnapshots` to the functional_test_runner command ## Linting diff --git a/x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js b/x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js index 39548717aa2f4..98469008e3412 100644 --- a/x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js +++ b/x-pack/plugins/apm/scripts/ftr_e2e/cypress_run.js @@ -5,12 +5,15 @@ * 2.0. */ +const { argv } = require('yargs'); const childProcess = require('child_process'); const path = require('path'); +const { spec } = argv; + const e2eDir = path.join(__dirname, '../../ftr_e2e'); childProcess.execSync( - `node ../../../../scripts/functional_tests --config ./cypress_run.ts`, + `node ../../../../scripts/functional_tests --config ./cypress_run.ts --grep ${spec}`, { cwd: e2eDir, stdio: 'inherit' } ); From 9045f736e4ff0416b657f6567f3449a6f0ffb534 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 12 Aug 2021 10:24:59 -0600 Subject: [PATCH 06/91] [Metrics UI] Remove alert previews (#107978) * [Metrics UI] Remove alert previews * Fixing i18n strings * Adding back in isTooManyBucketsPreviewException for evaluate_alert * Fixing import * Removing more obsolete code * removing unused strings for i18n * removing unused range function --- .../infra/common/alerting/metrics/types.ts | 93 --- .../common/components/alert_preview.tsx | 530 ------------------ .../common/components/get_alert_preview.ts | 39 -- .../infra/public/alerting/common/index.ts | 48 -- .../inventory/components/expression.tsx | 26 +- .../metric_anomaly/components/expression.tsx | 34 +- .../components/expression.tsx | 43 +- x-pack/plugins/infra/server/infra_server.ts | 2 - .../infra/server/lib/alerting/common/types.ts | 8 - ...review_inventory_metric_threshold_alert.ts | 168 ------ .../preview_metric_threshold_alert.test.ts | 221 -------- .../preview_metric_threshold_alert.ts | 242 -------- .../alerting/metric_threshold/test_mocks.ts | 111 ---- .../infra/server/routes/alerting/index.ts | 8 - .../infra/server/routes/alerting/preview.ts | 195 ------- .../translations/translations/ja-JP.json | 28 - .../translations/translations/zh-CN.json | 33 -- 17 files changed, 7 insertions(+), 1822 deletions(-) delete mode 100644 x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx delete mode 100644 x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts delete mode 100644 x-pack/plugins/infra/public/alerting/common/index.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts delete mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts delete mode 100644 x-pack/plugins/infra/server/routes/alerting/index.ts delete mode 100644 x-pack/plugins/infra/server/routes/alerting/preview.ts diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 94ec40dd2847e..2e8ad1de3413c 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -6,7 +6,6 @@ */ import * as rt from 'io-ts'; import { ANOMALY_THRESHOLD } from '../../infra_ml'; -import { ItemTypeRT } from '../../inventory_models/types'; // TODO: Have threshold and inventory alerts import these types from this file instead of from their // local directories @@ -55,95 +54,3 @@ export interface MetricAnomalyParams { threshold: Exclude; influencerFilter: rt.TypeOf | undefined; } - -// Alert Preview API -const baseAlertRequestParamsRT = rt.intersection([ - rt.partial({ - filterQuery: rt.union([rt.string, rt.undefined]), - sourceId: rt.string, - }), - rt.type({ - lookback: rt.union([ - rt.literal('ms'), - rt.literal('s'), - rt.literal('m'), - rt.literal('h'), - rt.literal('d'), - rt.literal('w'), - rt.literal('M'), - rt.literal('y'), - ]), - alertInterval: rt.string, - alertThrottle: rt.string, - alertOnNoData: rt.boolean, - alertNotifyWhen: rt.string, - }), -]); - -const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([ - baseAlertRequestParamsRT, - rt.partial({ - groupBy: rt.union([rt.string, rt.array(rt.string), rt.undefined]), - }), - rt.type({ - alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID), - criteria: rt.array(rt.any), - }), -]); -export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf< - typeof metricThresholdAlertPreviewRequestParamsRT ->; - -const inventoryAlertPreviewRequestParamsRT = rt.intersection([ - baseAlertRequestParamsRT, - rt.type({ - nodeType: ItemTypeRT, - alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID), - criteria: rt.array(rt.any), - }), -]); -export type InventoryAlertPreviewRequestParams = rt.TypeOf< - typeof inventoryAlertPreviewRequestParamsRT ->; - -const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([ - baseAlertRequestParamsRT, - rt.type({ - nodeType: metricAnomalyNodeTypeRT, - metric: metricAnomalyMetricRT, - threshold: rt.number, - alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID), - spaceId: rt.string, - }), - rt.partial({ - influencerFilter: metricAnomalyInfluencerFilterRT, - }), -]); -export type MetricAnomalyAlertPreviewRequestParams = rt.TypeOf< - typeof metricAnomalyAlertPreviewRequestParamsRT ->; - -export const alertPreviewRequestParamsRT = rt.union([ - metricThresholdAlertPreviewRequestParamsRT, - inventoryAlertPreviewRequestParamsRT, - metricAnomalyAlertPreviewRequestParamsRT, -]); -export type AlertPreviewRequestParams = rt.TypeOf; - -export const alertPreviewSuccessResponsePayloadRT = rt.type({ - numberOfGroups: rt.number, - resultTotals: rt.intersection([ - rt.type({ - fired: rt.number, - noData: rt.number, - error: rt.number, - notifications: rt.number, - }), - rt.partial({ - warning: rt.number, - }), - ]), -}); -export type AlertPreviewSuccessResponsePayload = rt.TypeOf< - typeof alertPreviewSuccessResponsePayloadRT ->; diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx deleted file mode 100644 index 7d7b0004fa068..0000000000000 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ /dev/null @@ -1,530 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { omit } from 'lodash'; -import React, { useCallback, useMemo, useState } from 'react'; -import { - EuiSpacer, - EuiFormRow, - EuiButton, - EuiSelect, - EuiFlexGroup, - EuiFlexItem, - EuiCallOut, - EuiAccordion, - EuiCodeBlock, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { AlertNotifyWhenType } from '../../../../../alerting/common'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { FORMATTERS } from '../../../../common/formatters'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; -import { - AlertPreviewSuccessResponsePayload, - AlertPreviewRequestParams, -} from '../../../../common/alerting/metrics'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getIntervalInSeconds } from '../../../../server/utils/get_interval_in_seconds'; -import { getAlertPreview, PreviewableAlertTypes } from './get_alert_preview'; - -interface Props { - alertInterval: string; - alertThrottle: string; - alertNotifyWhen: AlertNotifyWhenType; - alertType: PreviewableAlertTypes; - alertParams: { criteria?: any[]; sourceId: string } & Record; - validate: (params: any) => ValidationResult; - showNoDataResults?: boolean; - groupByDisplayName?: string; -} - -export const AlertPreview: React.FC = (props) => { - const { - alertParams, - alertInterval, - alertThrottle, - alertNotifyWhen, - alertType, - validate, - showNoDataResults, - groupByDisplayName, - } = props; - const { http } = useKibana().services; - - const [previewLookbackInterval, setPreviewLookbackInterval] = useState('h'); - const [isPreviewLoading, setIsPreviewLoading] = useState(false); - const [previewError, setPreviewError] = useState(false); - const [previewResult, setPreviewResult] = useState< - (AlertPreviewSuccessResponsePayload & Record) | null - >(null); - - const onSelectPreviewLookbackInterval = useCallback((e) => { - setPreviewLookbackInterval(e.target.value); - }, []); - - const onClickPreview = useCallback(async () => { - setIsPreviewLoading(true); - setPreviewResult(null); - setPreviewError(false); - try { - const result = await getAlertPreview({ - fetch: http!.fetch, - params: { - ...alertParams, - lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M', - alertInterval, - alertThrottle, - alertNotifyWhen, - alertOnNoData: showNoDataResults ?? false, - } as AlertPreviewRequestParams, - alertType, - }); - setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval, alertThrottle }); - } catch (e) { - setPreviewError(e); - } finally { - setIsPreviewLoading(false); - } - }, [ - alertParams, - alertInterval, - alertType, - alertNotifyWhen, - groupByDisplayName, - previewLookbackInterval, - alertThrottle, - showNoDataResults, - http, - ]); - - const previewIntervalError = useMemo(() => { - const intervalInSeconds = getIntervalInSeconds(alertInterval); - const lookbackInSeconds = getIntervalInSeconds(`1${previewLookbackInterval}`); - if (intervalInSeconds >= lookbackInSeconds) { - return true; - } - return false; - }, [previewLookbackInterval, alertInterval]); - - const isPreviewDisabled = useMemo(() => { - if (!alertParams.criteria) return false; - const validationResult = validate({ criteria: alertParams.criteria } as any); - const hasValidationErrors = Object.values(validationResult.errors).some((result) => - Object.values(result).some((arr) => Array.isArray(arr) && arr.length) - ); - return hasValidationErrors || previewIntervalError; - }, [alertParams.criteria, previewIntervalError, validate]); - - const showNumberOfNotifications = useMemo(() => { - if (!previewResult) return false; - if (alertNotifyWhen === 'onActiveAlert') return false; - const { notifications, fired, noData, error } = previewResult.resultTotals; - const unthrottledNotifications = fired + (showNoDataResults ? noData + error : 0); - return unthrottledNotifications > notifications; - }, [previewResult, showNoDataResults, alertNotifyWhen]); - - const hasWarningThreshold = useMemo( - () => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')) ?? false, - [alertParams] - ); - - return ( - - <> - - - - - - - {i18n.translate('xpack.infra.metrics.alertFlyout.testAlertCondition', { - defaultMessage: 'Test alert condition', - })} - - - - - {previewResult && !previewIntervalError && ( - <> - - - } - > - {showNoDataResults && previewResult.resultTotals.noData ? ( - - ), - boldedResultsNumber: ( - - {i18n.translate( - 'xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber', - { - defaultMessage: '{noData, plural, one {# result} other {# results}}', - values: { - noData: previewResult.resultTotals.noData, - }, - } - )} - - ), - }} - /> - ) : null}{' '} - {previewResult.resultTotals.error ? ( - - ) : null} - {showNumberOfNotifications ? ( - <> - - - {i18n.translate( - 'xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber', - { - defaultMessage: - '{notifs, plural, one {# notification} other {# notifications}}', - values: { - notifs: previewResult.resultTotals.notifications, - }, - } - )} - - ), - }} - /> - - ) : null}{' '} - - - )} - {previewIntervalError && ( - <> - - - } - color="warning" - iconType="help" - > - check every, - }} - /> - - - )} - {previewError && ( - <> - - {previewError.body?.statusCode === 508 ? ( - - } - color="warning" - iconType="help" - > - FOR THE LAST, - }} - /> - - ) : ( - - } - color="danger" - iconType="alert" - > - {previewError.body && ( - <> - - - - - - - - } - > - - {previewError.body.message} - - - )} - - )} - - )} - - - ); -}; - -const PreviewTextString = ({ - previewResult, - hasWarningThreshold, -}: { - previewResult: AlertPreviewSuccessResponsePayload & Record; - hasWarningThreshold: boolean; -}) => { - const instanceCount = hasWarningThreshold ? ( - - ), - criticalInstances: ( - - - - ), - warningInstances: ( - - - - ), - boldCritical: ( - - - - ), - boldWarning: ( - - - - ), - }} - /> - ) : ( - - ), - firedTimes: ( - - - - ), - }} - /> - ); - - const groupByText = previewResult.groupByDisplayName ? ( - <> - - - - ), - }} - />{' '} - - ) : ( - <> - ); - - const lookbackText = ( - e.value === previewResult.previewLookbackInterval) - ?.shortText, - }} - /> - ); - - return ( - - ); -}; - -const previewOptions = [ - { - value: 'h', - text: i18n.translate('xpack.infra.metrics.alertFlyout.lastHourLabel', { - defaultMessage: 'Last hour', - }), - shortText: i18n.translate('xpack.infra.metrics.alertFlyout.hourLabel', { - defaultMessage: 'hour', - }), - }, - { - value: 'd', - text: i18n.translate('xpack.infra.metrics.alertFlyout.lastDayLabel', { - defaultMessage: 'Last day', - }), - shortText: i18n.translate('xpack.infra.metrics.alertFlyout.dayLabel', { - defaultMessage: 'day', - }), - }, - { - value: 'w', - text: i18n.translate('xpack.infra.metrics.alertFlyout.lastWeekLabel', { - defaultMessage: 'Last week', - }), - shortText: i18n.translate('xpack.infra.metrics.alertFlyout.weekLabel', { - defaultMessage: 'week', - }), - }, - { - value: 'M', - text: i18n.translate('xpack.infra.metrics.alertFlyout.lastMonthLabel', { - defaultMessage: 'Last month', - }), - shortText: i18n.translate('xpack.infra.metrics.alertFlyout.monthLabel', { - defaultMessage: 'month', - }), - }, -]; - -const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) => - omit(o, 'shortText') -); diff --git a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts b/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts deleted file mode 100644 index 2bb98e83cbe70..0000000000000 --- a/x-pack/plugins/infra/public/alerting/common/components/get_alert_preview.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpHandler } from 'src/core/public'; -import { - INFRA_ALERT_PREVIEW_PATH, - METRIC_THRESHOLD_ALERT_TYPE_ID, - METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - METRIC_ANOMALY_ALERT_TYPE_ID, - AlertPreviewRequestParams, - AlertPreviewSuccessResponsePayload, -} from '../../../../common/alerting/metrics'; - -export type PreviewableAlertTypes = - | typeof METRIC_THRESHOLD_ALERT_TYPE_ID - | typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID - | typeof METRIC_ANOMALY_ALERT_TYPE_ID; - -export async function getAlertPreview({ - fetch, - params, - alertType, -}: { - fetch: HttpHandler; - params: AlertPreviewRequestParams; - alertType: PreviewableAlertTypes; -}): Promise { - return await fetch(`${INFRA_ALERT_PREVIEW_PATH}`, { - method: 'POST', - body: JSON.stringify({ - ...params, - alertType, - }), - }); -} diff --git a/x-pack/plugins/infra/public/alerting/common/index.ts b/x-pack/plugins/infra/public/alerting/common/index.ts deleted file mode 100644 index 3dc508e9b21f6..0000000000000 --- a/x-pack/plugins/infra/public/alerting/common/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -export { AlertPreview } from './components/alert_preview'; - -export const previewOptions = [ - { - value: 'h', - text: i18n.translate('xpack.infra.metrics.alertFlyout.lastHourLabel', { - defaultMessage: 'Last hour', - }), - shortText: i18n.translate('xpack.infra.metrics.alertFlyout.hourLabel', { - defaultMessage: 'hour', - }), - }, - { - value: 'd', - text: i18n.translate('xpack.infra.metrics.alertFlyout.lastDayLabel', { - defaultMessage: 'Last day', - }), - shortText: i18n.translate('xpack.infra.metrics.alertFlyout.dayLabel', { - defaultMessage: 'day', - }), - }, - { - value: 'w', - text: i18n.translate('xpack.infra.metrics.alertFlyout.lastWeekLabel', { - defaultMessage: 'Last week', - }), - shortText: i18n.translate('xpack.infra.metrics.alertFlyout.weekLabel', { - defaultMessage: 'week', - }), - }, - { - value: 'M', - text: i18n.translate('xpack.infra.metrics.alertFlyout.lastMonthLabel', { - defaultMessage: 'Last month', - }), - shortText: i18n.translate('xpack.infra.metrics.alertFlyout.monthLabel', { - defaultMessage: 'month', - }), - }, -]; diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index c4f8b5a615b0f..78005d9d87cf9 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { debounce, pick, omit } from 'lodash'; +import { debounce, omit } from 'lodash'; import { Unit } from '@elastic/datemath'; import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react'; import { IFieldType } from 'src/plugins/data/public'; @@ -26,8 +26,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; -import { AlertPreview } from '../../common'; -import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics'; import { Comparator, // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -68,7 +66,6 @@ import { SnapshotCustomMetricInputRT, } from '../../../../common/http_api/snapshot_api'; -import { validateMetricThreshold } from './validation'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { ExpressionChart } from './expression_chart'; @@ -113,15 +110,7 @@ export const defaultExpression = { export const Expressions: React.FC = (props) => { const { http, notifications } = useKibanaContextForPlugin().services; - const { - setAlertParams, - alertParams, - errors, - alertInterval, - alertThrottle, - metadata, - alertNotifyWhen, - } = props; + const { setAlertParams, alertParams, errors, metadata } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', fetch: http.fetch, @@ -403,17 +392,6 @@ export const Expressions: React.FC = (props) => { - - ); }; diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx index e44a747aa07e7..9f5c47554ac56 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/components/expression.tsx @@ -5,18 +5,13 @@ * 2.0. */ -import { pick } from 'lodash'; import React, { useCallback, useState, useMemo, useEffect } from 'react'; import { EuiFlexGroup, EuiSpacer, EuiText, EuiLoadingContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities'; import { SubscriptionSplashPrompt } from '../../../components/subscription_splash_content'; -import { AlertPreview } from '../../common'; -import { - METRIC_ANOMALY_ALERT_TYPE_ID, - MetricAnomalyParams, -} from '../../../../common/alerting/metrics'; +import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; import { euiStyled, EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { WhenExpression, @@ -35,7 +30,6 @@ import { SeverityThresholdExpression } from './severity_threshold'; import { InfraWaffleMapOptions } from '../../../lib/lib'; import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; -import { validateMetricAnomaly } from './validation'; import { InfluencerFilter } from './influencer_filter'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; @@ -65,14 +59,7 @@ export const Expression: React.FC = (props) => { const { http, notifications } = useKibanaContextForPlugin().services; const { space } = useActiveKibanaSpace(); - const { - setAlertParams, - alertParams, - alertInterval, - alertThrottle, - alertNotifyWhen, - metadata, - } = props; + const { setAlertParams, alertParams, alertInterval, metadata } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', fetch: http.fetch, @@ -258,23 +245,6 @@ export const Expression: React.FC = (props) => { onChangeFieldValue={updateInfluencerFieldValue} /> - - ); }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index be0ecbb1dab65..58bc476f151bd 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { debounce, pick } from 'lodash'; +import { debounce } from 'lodash'; import { Unit } from '@elastic/datemath'; import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react'; import { @@ -22,12 +22,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AlertPreview } from '../../common'; -import { - Comparator, - Aggregators, - METRIC_THRESHOLD_ALERT_TYPE_ID, -} from '../../../../common/alerting/metrics'; +import { Comparator, Aggregators } from '../../../../common/alerting/metrics'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { IErrorObject, @@ -43,7 +38,6 @@ import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; import { ExpressionRow } from './expression_row'; import { MetricExpression, AlertParams, AlertContextMeta } from '../types'; import { ExpressionChart } from './expression_chart'; -import { validateMetricThreshold } from './validation'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; const FILTER_TYPING_DEBOUNCE_MS = 500; @@ -63,15 +57,7 @@ const defaultExpression = { export { defaultExpression }; export const Expressions: React.FC = (props) => { - const { - setAlertParams, - alertParams, - errors, - alertInterval, - alertThrottle, - metadata, - alertNotifyWhen, - } = props; + const { setAlertParams, alertParams, errors, metadata } = props; const { http, notifications } = useKibanaContextForPlugin().services; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', @@ -256,11 +242,6 @@ export const Expressions: React.FC = (props) => { [onFilterChange] ); - const groupByPreviewDisplayName = useMemo(() => { - if (Array.isArray(alertParams.groupBy)) return alertParams.groupBy.join(', '); - return alertParams.groupBy; - }, [alertParams.groupBy]); - const areAllAggsRate = useMemo( () => alertParams.criteria?.every((c) => c.aggType === Aggregators.RATE), [alertParams.criteria] @@ -435,24 +416,6 @@ export const Expressions: React.FC = (props) => { - - ); }; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index f42207e0ad142..d289cf339851d 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -34,7 +34,6 @@ import { initInventoryMetaRoute } from './routes/inventory_metadata'; import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; import { initMetricsSourceConfigurationRoutes } from './routes/metrics_sources'; import { initOverviewRoute } from './routes/overview'; -import { initAlertPreviewRoute } from './routes/alerting'; import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; import { initProcessListRoute } from './routes/process_list'; @@ -63,7 +62,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initInventoryMetaRoute(libs); initLogSourceConfigurationRoutes(libs); initLogSourceStatusRoutes(libs); - initAlertPreviewRoute(libs); initGetLogAlertsChartPreviewDataRoute(libs); initProcessListRoute(libs); initOverviewRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/alerting/common/types.ts b/x-pack/plugins/infra/server/lib/alerting/common/types.ts index 0b809429de0d2..1d038cace14fe 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/types.ts @@ -33,11 +33,3 @@ export enum AlertStates { NO_DATA, ERROR, } - -export interface PreviewResult { - fired: number; - warning: number; - noData: number; - error: number; - notifications: number; -} diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts deleted file mode 100644 index 00d01b15750d1..0000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Unit } from '@elastic/datemath'; -import { first } from 'lodash'; -import { PreviewResult } from '../common/types'; -import { InventoryMetricConditions } from './types'; -import { - TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, - isTooManyBucketsPreviewException, -} from '../../../../common/alerting/metrics'; -import { ElasticsearchClient } from '../../../../../../../src/core/server'; -import { InfraSource } from '../../../../common/source_configuration/source_configuration'; -import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { evaluateCondition } from './evaluate_condition'; -import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; - -interface InventoryMetricThresholdParams { - criteria: InventoryMetricConditions[]; - filterQuery: string | undefined; - nodeType: InventoryItemType; - sourceId?: string; -} - -interface PreviewInventoryMetricThresholdAlertParams { - esClient: ElasticsearchClient; - params: InventoryMetricThresholdParams; - source: InfraSource; - logQueryFields: LogQueryFields; - compositeSize: number; - lookback: Unit; - alertInterval: string; - alertThrottle: string; - alertOnNoData: boolean; - alertNotifyWhen: string; -} - -export const previewInventoryMetricThresholdAlert: ( - params: PreviewInventoryMetricThresholdAlertParams -) => Promise = async ({ - esClient, - params, - source, - logQueryFields, - compositeSize, - lookback, - alertInterval, - alertThrottle, - alertOnNoData, - alertNotifyWhen, -}: PreviewInventoryMetricThresholdAlertParams) => { - const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams; - - if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); - - const { timeSize, timeUnit } = criteria[0]; - const bucketInterval = `${timeSize}${timeUnit}`; - const bucketIntervalInSeconds = getIntervalInSeconds(bucketInterval); - - const lookbackInterval = `1${lookback}`; - const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval); - const lookbackSize = Math.ceil(lookbackIntervalInSeconds / bucketIntervalInSeconds); - - const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); - const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; - const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle); - - try { - const results = await Promise.all( - criteria.map((condition) => - evaluateCondition({ - condition, - nodeType, - source, - logQueryFields, - esClient, - compositeSize, - filterQuery, - lookbackSize, - }) - ) - ); - - const inventoryItems = Object.keys(first(results)!); - const previewResults = inventoryItems.map((item) => { - const numberOfResultBuckets = lookbackSize; - const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution); - let numberOfTimesFired = 0; - let numberOfTimesWarned = 0; - let numberOfNoDataResults = 0; - let numberOfErrors = 0; - let numberOfNotifications = 0; - let throttleTracker = 0; - let previousActionGroup: string | null = null; - const notifyWithThrottle = (actionGroup: string) => { - if (alertNotifyWhen === 'onActionGroupChange') { - if (previousActionGroup !== actionGroup) numberOfNotifications++; - } else if (alertNotifyWhen === 'onThrottleInterval') { - if (throttleTracker === 0) numberOfNotifications++; - throttleTracker += alertIntervalInSeconds; - } else { - numberOfNotifications++; - } - previousActionGroup = actionGroup; - }; - for (let i = 0; i < numberOfExecutionBuckets; i++) { - const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); - const allConditionsFiredInMappedBucket = results.every((result) => { - const shouldFire = result[item].shouldFire as boolean[]; - return shouldFire[mappedBucketIndex]; - }); - const allConditionsWarnInMappedBucket = - !allConditionsFiredInMappedBucket && - results.every((result) => result[item].shouldWarn[mappedBucketIndex]); - const someConditionsNoDataInMappedBucket = results.some((result) => { - const hasNoData = result[item].isNoData as boolean[]; - return hasNoData[mappedBucketIndex]; - }); - const someConditionsErrorInMappedBucket = results.some((result) => { - return result[item].isError; - }); - if (someConditionsErrorInMappedBucket) { - numberOfErrors++; - if (alertOnNoData) { - notifyWithThrottle('fired'); // TODO: Update this when No Data alerts move to an action group - } - } else if (someConditionsNoDataInMappedBucket) { - numberOfNoDataResults++; - if (alertOnNoData) { - notifyWithThrottle('fired'); // TODO: Update this when No Data alerts move to an action group - } - } else if (allConditionsFiredInMappedBucket) { - numberOfTimesFired++; - notifyWithThrottle('fired'); - } else if (allConditionsWarnInMappedBucket) { - numberOfTimesWarned++; - notifyWithThrottle('warning'); - } else { - previousActionGroup = 'recovered'; - if (throttleTracker > 0) { - throttleTracker += alertIntervalInSeconds; - } - } - if (throttleTracker >= throttleIntervalInSeconds) { - throttleTracker = 0; - } - } - return { - fired: numberOfTimesFired, - warning: numberOfTimesWarned, - noData: numberOfNoDataResults, - error: numberOfErrors, - notifications: numberOfNotifications, - }; - }); - - return previewResults; - } catch (e) { - if (!isTooManyBucketsPreviewException(e)) throw e; - const { maxBuckets } = e; - throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets}`); - } -}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts deleted file mode 100644 index e03b475191dff..0000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as mocks from './test_mocks'; -import { Comparator, Aggregators, MetricExpressionParams } from './types'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; -import { previewMetricThresholdAlert } from './preview_metric_threshold_alert'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; - -describe('Previewing the metric threshold alert type', () => { - describe('querying the entire infrastructure', () => { - test('returns the expected results using a bucket interval equal to the alert interval', async () => { - const [ungroupedResult] = await previewMetricThresholdAlert({ - ...baseParams, - lookback: 'h', - alertInterval: '1m', - alertThrottle: '1m', - alertOnNoData: true, - alertNotifyWhen: 'onThrottleInterval', - }); - const { fired, noData, error, notifications } = ungroupedResult; - expect(fired).toBe(30); - expect(noData).toBe(0); - expect(error).toBe(0); - expect(notifications).toBe(30); - }); - - test('returns the expected results using a bucket interval shorter than the alert interval', async () => { - const [ungroupedResult] = await previewMetricThresholdAlert({ - ...baseParams, - lookback: 'h', - alertInterval: '3m', - alertThrottle: '3m', - alertOnNoData: true, - alertNotifyWhen: 'onThrottleInterval', - }); - const { fired, noData, error, notifications } = ungroupedResult; - expect(fired).toBe(10); - expect(noData).toBe(0); - expect(error).toBe(0); - expect(notifications).toBe(10); - }); - test('returns the expected results using a bucket interval longer than the alert interval', async () => { - const [ungroupedResult] = await previewMetricThresholdAlert({ - ...baseParams, - lookback: 'h', - alertInterval: '30s', - alertThrottle: '30s', - alertOnNoData: true, - alertNotifyWhen: 'onThrottleInterval', - }); - const { fired, noData, error, notifications } = ungroupedResult; - expect(fired).toBe(60); - expect(noData).toBe(0); - expect(error).toBe(0); - expect(notifications).toBe(60); - }); - test('returns the expected results using a throttle interval longer than the alert interval', async () => { - const [ungroupedResult] = await previewMetricThresholdAlert({ - ...baseParams, - lookback: 'h', - alertInterval: '1m', - alertThrottle: '3m', - alertOnNoData: true, - alertNotifyWhen: 'onThrottleInterval', - }); - const { fired, noData, error, notifications } = ungroupedResult; - expect(fired).toBe(30); - expect(noData).toBe(0); - expect(error).toBe(0); - expect(notifications).toBe(15); - }); - test('returns the expected results using a notify setting of Only on Status Change', async () => { - const [ungroupedResult] = await previewMetricThresholdAlert({ - ...baseParams, - params: { - ...baseParams.params, - criteria: [ - { - ...baseCriterion, - metric: 'test.metric.3', - } as MetricExpressionParams, - ], - }, - lookback: 'h', - alertInterval: '1m', - alertThrottle: '1m', - alertOnNoData: true, - alertNotifyWhen: 'onActionGroupChange', - }); - const { fired, noData, error, notifications } = ungroupedResult; - expect(fired).toBe(20); - expect(noData).toBe(0); - expect(error).toBe(0); - expect(notifications).toBe(20); - }); - }); - describe('querying with a groupBy parameter', () => { - test('returns the expected results', async () => { - const [resultA, resultB] = await previewMetricThresholdAlert({ - ...baseParams, - params: { - ...baseParams.params, - groupBy: ['something'], - }, - lookback: 'h', - alertInterval: '1m', - alertThrottle: '1m', - alertOnNoData: true, - alertNotifyWhen: 'onThrottleInterval', - }); - const { - fired: firedA, - noData: noDataA, - error: errorA, - notifications: notificationsA, - } = resultA; - expect(firedA).toBe(30); - expect(noDataA).toBe(0); - expect(errorA).toBe(0); - expect(notificationsA).toBe(30); - const { - fired: firedB, - noData: noDataB, - error: errorB, - notifications: notificationsB, - } = resultB; - expect(firedB).toBe(60); - expect(noDataB).toBe(0); - expect(errorB).toBe(0); - expect(notificationsB).toBe(60); - }); - }); - describe('querying a data set with a period of No Data', () => { - test('returns the expected results', async () => { - const [ungroupedResult] = await previewMetricThresholdAlert({ - ...baseParams, - params: { - ...baseParams.params, - criteria: [ - { - ...baseCriterion, - metric: 'test.metric.2', - } as MetricExpressionParams, - ], - }, - lookback: 'h', - alertInterval: '1m', - alertThrottle: '1m', - alertOnNoData: true, - alertNotifyWhen: 'onThrottleInterval', - }); - const { fired, noData, error, notifications } = ungroupedResult; - expect(fired).toBe(25); - expect(noData).toBe(10); - expect(error).toBe(0); - expect(notifications).toBe(35); - }); - }); -}); - -const services: AlertServicesMock = alertsMock.createAlertServices(); - -services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => { - const from = params?.body.query.bool.filter[0]?.range['@timestamp'].gte; - const metric = params?.body.query.bool.filter[1]?.exists.field; - if (params?.body.aggs.groupings) { - if (params?.body.aggs.groupings.composite.after) { - return elasticsearchClientMock.createSuccessTransportRequestPromise( - mocks.compositeEndResponse - ); - } - return elasticsearchClientMock.createSuccessTransportRequestPromise( - mocks.basicCompositePreviewResponse(from) - ); - } - if (metric === 'test.metric.2') { - return elasticsearchClientMock.createSuccessTransportRequestPromise( - mocks.alternateMetricPreviewResponse(from) - ); - } - if (metric === 'test.metric.3') { - return elasticsearchClientMock.createSuccessTransportRequestPromise( - mocks.repeatingMetricPreviewResponse(from) - ); - } - return elasticsearchClientMock.createSuccessTransportRequestPromise( - mocks.basicMetricPreviewResponse(from) - ); -}); - -const baseCriterion = { - aggType: Aggregators.AVERAGE, - metric: 'test.metric.1', - timeSize: 1, - timeUnit: 'm', - comparator: Comparator.GT, - threshold: [0.75], -} as MetricExpressionParams; - -const config = { - metricAlias: 'metricbeat-*', - fields: { - timefield: '@timestamp', - }, -} as any; - -const baseParams = { - esClient: services.scopedClusterClient.asCurrentUser, - params: { - criteria: [baseCriterion], - groupBy: undefined, - filterQuery: undefined, - }, - config, -}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts deleted file mode 100644 index 931b830875cdf..0000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { first, zip } from 'lodash'; -import { Unit } from '@elastic/datemath'; -import { - TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, - isTooManyBucketsPreviewException, -} from '../../../../common/alerting/metrics'; -import { ElasticsearchClient } from '../../../../../../../src/core/server'; -import { InfraSource } from '../../../../common/source_configuration/source_configuration'; -import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; -import { PreviewResult } from '../common/types'; -import { MetricExpressionParams } from './types'; -import { evaluateAlert } from './lib/evaluate_alert'; - -const MAX_ITERATIONS = 50; - -interface PreviewMetricThresholdAlertParams { - esClient: ElasticsearchClient; - params: { - criteria: MetricExpressionParams[]; - groupBy: string | undefined | string[]; - filterQuery: string | undefined; - shouldDropPartialBuckets?: boolean; - }; - config: InfraSource['configuration']; - lookback: Unit; - alertInterval: string; - alertThrottle: string; - alertNotifyWhen: string; - alertOnNoData: boolean; - end?: number; - overrideLookbackIntervalInSeconds?: number; -} - -export const previewMetricThresholdAlert: ( - params: PreviewMetricThresholdAlertParams, - iterations?: number, - precalculatedNumberOfGroups?: number -) => Promise = async ( - { - esClient, - params, - config, - lookback, - alertInterval, - alertThrottle, - alertNotifyWhen, - alertOnNoData, - end = Date.now(), - overrideLookbackIntervalInSeconds, - }, - iterations = 0, - precalculatedNumberOfGroups -) => { - if (params.criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); - - // There are three different "intervals" we're dealing with here, so to disambiguate: - // - The lookback interval, which is how long of a period of time we want to examine to count - // how many times the alert fired - // - The interval in the alert params, which we'll call the bucket interval; this is how large of - // a time bucket the alert uses to evaluate its result - // - The alert interval, which is how often the alert fires - - const { timeSize, timeUnit } = params.criteria[0]; - const bucketInterval = `${timeSize}${timeUnit}`; - const bucketIntervalInSeconds = getIntervalInSeconds(bucketInterval); - - const lookbackInterval = `1${lookback}`; - const lookbackIntervalInSeconds = - overrideLookbackIntervalInSeconds ?? getIntervalInSeconds(lookbackInterval); - - const start = end - lookbackIntervalInSeconds * 1000; - const timeframe = { start, end }; - - // Get a date histogram using the bucket interval and the lookback interval - try { - const alertResults = await evaluateAlert(esClient, params, config, timeframe); - const groups = Object.keys(first(alertResults)!); - - // Now determine how to interpolate this histogram based on the alert interval - const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); - const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; - const throttleIntervalInSeconds = Math.max( - getIntervalInSeconds(alertThrottle), - alertIntervalInSeconds - ); - - const previewResults = await Promise.all( - groups.map(async (group) => { - // Interpolate the buckets returned by evaluateAlert and return a count of how many of these - // buckets would have fired the alert. If the alert interval and bucket interval are the same, - // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation - // will skip some buckets or read some buckets more than once, depending on the differential - const numberOfResultBuckets = first(alertResults)![group].shouldFire.length; - const numberOfExecutionBuckets = Math.floor( - numberOfResultBuckets / alertResultsPerExecution - ); - let numberOfTimesFired = 0; - let numberOfTimesWarned = 0; - let numberOfNoDataResults = 0; - let numberOfErrors = 0; - let numberOfNotifications = 0; - let throttleTracker = 0; - let previousActionGroup: string | null = null; - const notifyWithThrottle = (actionGroup: string) => { - if (alertNotifyWhen === 'onActionGroupChange') { - if (previousActionGroup !== actionGroup) numberOfNotifications++; - previousActionGroup = actionGroup; - } else if (alertNotifyWhen === 'onThrottleInterval') { - if (throttleTracker === 0) numberOfNotifications++; - throttleTracker += alertIntervalInSeconds; - } else { - numberOfNotifications++; - } - }; - for (let i = 0; i < numberOfExecutionBuckets; i++) { - const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); - const allConditionsFiredInMappedBucket = alertResults.every( - (alertResult) => alertResult[group].shouldFire[mappedBucketIndex] - ); - const allConditionsWarnInMappedBucket = - !allConditionsFiredInMappedBucket && - alertResults.every((alertResult) => alertResult[group].shouldWarn[mappedBucketIndex]); - const someConditionsNoDataInMappedBucket = alertResults.some((alertResult) => { - const hasNoData = alertResult[group].isNoData as boolean[]; - return hasNoData[mappedBucketIndex]; - }); - const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => { - return alertResult[group].isError; - }); - if (someConditionsErrorInMappedBucket) { - numberOfErrors++; - if (alertOnNoData) { - notifyWithThrottle('fired'); // TODO: Update this when No Data alerts move to an action group - } - } else if (someConditionsNoDataInMappedBucket) { - numberOfNoDataResults++; - if (alertOnNoData) { - notifyWithThrottle('fired'); // TODO: Update this when No Data alerts move to an action group - } - } else if (allConditionsFiredInMappedBucket) { - numberOfTimesFired++; - notifyWithThrottle('fired'); - } else if (allConditionsWarnInMappedBucket) { - numberOfTimesWarned++; - notifyWithThrottle('warning'); - } else { - previousActionGroup = 'recovered'; - if (throttleTracker > 0) { - throttleTracker += alertIntervalInSeconds; - } - } - if (throttleTracker >= throttleIntervalInSeconds) { - throttleTracker = 0; - } - } - return { - fired: numberOfTimesFired, - warning: numberOfTimesWarned, - noData: numberOfNoDataResults, - error: numberOfErrors, - notifications: numberOfNotifications, - }; - }) - ); - return previewResults; - } catch (e) { - if (isTooManyBucketsPreviewException(e)) { - // If there's too much data on the first request, recursively slice the lookback interval - // until all the data can be retrieved - const basePreviewParams = { - esClient, - params, - config, - lookback, - alertInterval, - alertThrottle, - alertOnNoData, - alertNotifyWhen, - }; - const { maxBuckets } = e; - // If this is still the first iteration, try to get the number of groups in order to - // calculate max buckets. If this fails, just estimate based on 1 group - const currentAlertResults = !precalculatedNumberOfGroups - ? await evaluateAlert(esClient, params, config) - : []; - const numberOfGroups = - precalculatedNumberOfGroups ?? Math.max(Object.keys(first(currentAlertResults)!).length, 1); - const estimatedTotalBuckets = - (lookbackIntervalInSeconds / bucketIntervalInSeconds) * numberOfGroups; - // The minimum number of slices is 2. In case we underestimate the total number of buckets - // in the first iteration, we can bisect the remaining buckets on further recursions to get - // all the data needed - const slices = Math.max(Math.ceil(estimatedTotalBuckets / maxBuckets), 2); - const slicedLookback = Math.floor(lookbackIntervalInSeconds / slices); - - // Bail out if it looks like this is going to take too long - if (slicedLookback <= 0 || iterations > MAX_ITERATIONS || slices > MAX_ITERATIONS) { - throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets * MAX_ITERATIONS}`); - } - - const slicedRequests = [...Array(slices)].map((_, i) => { - return previewMetricThresholdAlert( - { - ...basePreviewParams, - end: Math.min(end, start + slicedLookback * (i + 1) * 1000), - overrideLookbackIntervalInSeconds: slicedLookback, - }, - iterations + slices, - numberOfGroups - ); - }); - const results = await Promise.all(slicedRequests); - const zippedResult = zip(...results).map((result) => - result - // `undefined` values occur if there is no data at all in a certain slice, and that slice - // returns an empty array. This is different from an error or no data state, - // so filter these results out entirely and only regard the resultA portion - .filter( - (value: Value): value is NonNullable => typeof value !== 'undefined' - ) - .reduce((a, b) => { - if (!a) return b; - if (!b) return a; - const res = { ...a }; - const entries = (Object.entries(b) as unknown) as Array<[keyof PreviewResult, number]>; - for (const [key, value] of entries) { - res[key] += value; - } - return res; - }) - ); - return zippedResult; - } else throw e; - } -}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index 5af14b3fbd17d..409a4329aa65c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { range } from 'lodash'; const bucketsA = (from: number) => [ { doc_count: null, @@ -104,61 +103,6 @@ const bucketsC = (from: number) => [ }, ]; -const previewBucketsA = (from: number) => - range(from, from + 3600000, 60000).map((timestamp, i) => { - return { - doc_count: i % 2 ? 3 : 2, - aggregatedValue: { value: i % 2 ? 16 : 0.5 }, - from_as_string: new Date(timestamp).toISOString(), - }; - }); - -const previewBucketsB = (from: number) => - range(from, from + 3600000, 60000).map((timestamp, i) => { - const value = i % 2 ? 3.5 : 2.5; - return { - doc_count: i % 2 ? 3 : 2, - aggregatedValue: { value, values: [{ key: 99.0, value }] }, - from_as_string: new Date(timestamp).toISOString(), - }; - }); - -const previewBucketsWithNulls = (from: number) => [ - // 25 Fired - ...range(from, from + 1500000, 60000).map((timestamp) => { - return { - doc_count: 2, - aggregatedValue: { value: 1, values: [{ key: 95.0, value: 1 }] }, - from_as_string: new Date(timestamp).toISOString(), - }; - }), - // 25 OK - ...range(from + 2100000, from + 2940000, 60000).map((timestamp) => { - return { - doc_count: 2, - aggregatedValue: { value: 0.5, values: [{ key: 95.0, value: 0.5 }] }, - from_as_string: new Date(timestamp).toISOString(), - }; - }), - // 10 No Data - ...range(from + 3000000, from + 3600000, 60000).map((timestamp) => { - return { - doc_count: 0, - aggregatedValue: { value: null, values: [{ key: 95.0, value: null }] }, - from_as_string: new Date(timestamp).toISOString(), - }; - }), -]; - -const previewBucketsRepeat = (from: number) => - range(from, from + 3600000, 60000).map((timestamp, i) => { - return { - doc_count: i % 3 ? 3 : 2, - aggregatedValue: { value: i % 3 ? 0.5 : 16 }, - from_as_string: new Date(timestamp).toISOString(), - }; - }); - export const basicMetricResponse = (from: number) => ({ aggregations: { aggregatedIntervals: { @@ -271,58 +215,3 @@ export const changedSourceIdResponse = (from: number) => ({ }, }, }); - -export const basicMetricPreviewResponse = (from: number) => ({ - aggregations: { - aggregatedIntervals: { - buckets: previewBucketsA(from), - }, - }, -}); - -export const alternateMetricPreviewResponse = (from: number) => ({ - aggregations: { - aggregatedIntervals: { - buckets: previewBucketsWithNulls(from), - }, - }, -}); - -export const repeatingMetricPreviewResponse = (from: number) => ({ - aggregations: { - aggregatedIntervals: { - buckets: previewBucketsRepeat(from), - }, - }, -}); - -export const basicCompositePreviewResponse = (from: number) => ({ - aggregations: { - groupings: { - after_key: { groupBy0: 'foo' }, - buckets: [ - { - key: { - groupBy0: 'a', - }, - aggregatedIntervals: { - buckets: previewBucketsA(from), - }, - }, - { - key: { - groupBy0: 'b', - }, - aggregatedIntervals: { - buckets: previewBucketsB(from), - }, - }, - ], - }, - }, - hits: { - total: { - value: 2, - }, - }, -}); diff --git a/x-pack/plugins/infra/server/routes/alerting/index.ts b/x-pack/plugins/infra/server/routes/alerting/index.ts deleted file mode 100644 index 91092dac7a0da..0000000000000 --- a/x-pack/plugins/infra/server/routes/alerting/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './preview'; diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts deleted file mode 100644 index 4cafc743b1ecb..0000000000000 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PreviewResult } from '../../lib/alerting/common/types'; -import { - METRIC_THRESHOLD_ALERT_TYPE_ID, - METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - METRIC_ANOMALY_ALERT_TYPE_ID, - INFRA_ALERT_PREVIEW_PATH, - TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, - alertPreviewRequestParamsRT, - alertPreviewSuccessResponsePayloadRT, - MetricThresholdAlertPreviewRequestParams, - InventoryAlertPreviewRequestParams, - MetricAnomalyAlertPreviewRequestParams, -} from '../../../common/alerting/metrics'; -import { createValidationFunction } from '../../../common/runtime_types'; -import { previewInventoryMetricThresholdAlert } from '../../lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert'; -import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert'; -import { previewMetricAnomalyAlert } from '../../lib/alerting/metric_anomaly/preview_metric_anomaly_alert'; -import { InfraBackendLibs } from '../../lib/infra_types'; -import { assertHasInfraMlPlugins } from '../../utils/request_context'; - -export const initAlertPreviewRoute = ({ - framework, - sources, - getLogQueryFields, - configuration, -}: InfraBackendLibs) => { - framework.registerRoute( - { - method: 'post', - path: INFRA_ALERT_PREVIEW_PATH, - validate: { - body: createValidationFunction(alertPreviewRequestParamsRT), - }, - }, - framework.router.handleLegacyErrors(async (requestContext, request, response) => { - const { - lookback, - sourceId, - alertType, - alertInterval, - alertThrottle, - alertOnNoData, - alertNotifyWhen, - } = request.body; - - const esClient = requestContext.core.elasticsearch.client.asCurrentUser; - - const source = await sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId || 'default' - ); - - const compositeSize = configuration.inventory.compositeSize; - - try { - switch (alertType) { - case METRIC_THRESHOLD_ALERT_TYPE_ID: { - const { - groupBy, - criteria, - filterQuery, - } = request.body as MetricThresholdAlertPreviewRequestParams; - const previewResult = await previewMetricThresholdAlert({ - esClient, - params: { criteria, filterQuery, groupBy }, - lookback, - config: source.configuration, - alertInterval, - alertThrottle, - alertNotifyWhen, - alertOnNoData, - }); - - const payload = processPreviewResults(previewResult); - return response.ok({ - body: alertPreviewSuccessResponsePayloadRT.encode(payload), - }); - } - case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: { - const logQueryFields = await getLogQueryFields( - sourceId || 'default', - requestContext.core.savedObjects.client, - requestContext.core.elasticsearch.client.asCurrentUser - ); - const { - nodeType, - criteria, - filterQuery, - } = request.body as InventoryAlertPreviewRequestParams; - const previewResult = await previewInventoryMetricThresholdAlert({ - esClient, - params: { criteria, filterQuery, nodeType }, - lookback, - source, - logQueryFields, - compositeSize, - alertInterval, - alertThrottle, - alertNotifyWhen, - alertOnNoData, - }); - - const payload = processPreviewResults(previewResult); - - return response.ok({ - body: alertPreviewSuccessResponsePayloadRT.encode(payload), - }); - } - case METRIC_ANOMALY_ALERT_TYPE_ID: { - assertHasInfraMlPlugins(requestContext); - const { - nodeType, - metric, - threshold, - influencerFilter, - } = request.body as MetricAnomalyAlertPreviewRequestParams; - const { mlAnomalyDetectors, mlSystem, spaceId } = requestContext.infra; - - const previewResult = await previewMetricAnomalyAlert({ - mlAnomalyDetectors, - mlSystem, - spaceId, - params: { nodeType, metric, threshold, influencerFilter }, - lookback, - sourceId: source.id, - alertInterval, - alertThrottle, - alertOnNoData, - alertNotifyWhen, - }); - - return response.ok({ - body: alertPreviewSuccessResponsePayloadRT.encode({ - numberOfGroups: 1, - resultTotals: { - ...previewResult, - error: 0, - noData: 0, - }, - }), - }); - } - default: - throw new Error('Unknown alert type'); - } - } catch (error) { - if (error.message.includes(TOO_MANY_BUCKETS_PREVIEW_EXCEPTION)) { - return response.customError({ - statusCode: 508, - body: { - message: error.message.split(':')[1], // Extract the max buckets from the error message - }, - }); - } - return response.customError({ - statusCode: error.statusCode ?? 500, - body: { - message: error.message ?? 'An unexpected error occurred', - }, - }); - } - }) - ); -}; - -const processPreviewResults = (previewResult: PreviewResult[]) => { - const numberOfGroups = previewResult.length; - const resultTotals = previewResult.reduce( - (totals, { fired, warning, noData, error, notifications }) => { - return { - ...totals, - fired: totals.fired + fired, - warning: totals.warning + warning, - noData: totals.noData + noData, - error: totals.error + error, - notifications: totals.notifications + notifications, - }; - }, - { - fired: 0, - warning: 0, - noData: 0, - error: 0, - notifications: 0, - } - ); - return { numberOfGroups, resultTotals }; -}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 953b38225cd05..fbe13b7b961be 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12127,30 +12127,16 @@ "xpack.infra.metrics.alertFlyout.aggregationText.sum": "合計", "xpack.infra.metrics.alertFlyout.alertDescription": "メトリックアグリゲーションがしきい値を超えたときにアラートを発行します。", "xpack.infra.metrics.alertFlyout.alertOnNoData": "データがない場合に通知する", - "xpack.infra.metrics.alertFlyout.alertPreviewError": "このアラート条件をプレビューするときにエラーが発生しました", - "xpack.infra.metrics.alertFlyout.alertPreviewErrorDesc": "しばらくたってから再試行するか、詳細を確認してください。", - "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "一部のデータを評価するときにエラーが発生しました。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroupBy": "{groups}全体", - "xpack.infra.metrics.alertFlyout.alertPreviewLookback": "前回の{lookback}", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データなしの{wereWas} {boldedResultsNumber}がありました。", - "xpack.infra.metrics.alertFlyout.alertPreviewOnlyOnStatusChange": "ステータス変更時のみ", - "xpack.infra.metrics.alertFlyout.alertPreviewResultInstances": "このアラートの条件を満たした{wereWas} {firedTimes}がありました", - "xpack.infra.metrics.alertFlyout.alertPreviewResultText": "{instanceCount} {groupByWithConditionalTrailingSpace}{lookbackText}.", - "xpack.infra.metrics.alertFlyout.alertPreviewResultWithSeverityLevels": "このアラートの{boldCritical}条件を満たした{wereWas} {criticalInstances}と、{boldWarning}条件を満たした{warningInstances}", - "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications": "結果として、このアラートは、「{alertThrottle}」に関して選択した[通知]設定に基づいて{notifications}を送信しました。", "xpack.infra.metrics.alertFlyout.anomalyFilterHelpText": "アラートトリガーの範囲を、特定のノードの影響を受ける異常に制限します。", "xpack.infra.metrics.alertFlyout.anomalyFilterHelpTextExample": "例:「my-node-1」または「my-node-*」", "xpack.infra.metrics.alertFlyout.anomalyInfluencerFilterPlaceholder": "すべて", "xpack.infra.metrics.alertFlyout.anomalyJobs.memoryUsage": "メモリー使用状況", "xpack.infra.metrics.alertFlyout.anomalyJobs.networkIn": "内向きのネットワーク", "xpack.infra.metrics.alertFlyout.anomalyJobs.networkOut": "外向きのネットワーク", - "xpack.infra.metrics.alertFlyout.boldCritical": "致命的", - "xpack.infra.metrics.alertFlyout.boldWarning": "警告", "xpack.infra.metrics.alertFlyout.conditions": "条件", "xpack.infra.metrics.alertFlyout.createAlertPerHelpText": "すべての一意の値についてアラートを作成します。例:「host.id」または「cloud.region」。", "xpack.infra.metrics.alertFlyout.createAlertPerText": "次の単位でアラートを作成 (任意) ", "xpack.infra.metrics.alertFlyout.criticalThreshold": "アラート", - "xpack.infra.metrics.alertFlyout.dayLabel": "日", "xpack.infra.metrics.alertFlyout.error.aggregationRequired": "集約が必要です。", "xpack.infra.metrics.alertFlyout.error.customMetricFieldRequired": "フィールドが必要です。", "xpack.infra.metrics.alertFlyout.error.metricRequired": "メトリックが必要です。", @@ -12158,7 +12144,6 @@ "xpack.infra.metrics.alertFlyout.error.thresholdRequired": "しきい値が必要です。", "xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired": "しきい値には有効な数値を含める必要があります。", "xpack.infra.metrics.alertFlyout.error.timeRequred": "ページサイズが必要です。", - "xpack.infra.metrics.alertFlyout.errorDetails": "詳細", "xpack.infra.metrics.alertFlyout.expandRowLabel": "行を展開します。", "xpack.infra.metrics.alertFlyout.expression.for.descriptionLabel": "対象", "xpack.infra.metrics.alertFlyout.expression.for.popoverTitle": "ノードのタイプ", @@ -12174,26 +12159,13 @@ "xpack.infra.metrics.alertFlyout.filterByNodeLabel": "ノードでフィルタリング", "xpack.infra.metrics.alertFlyout.filterHelpText": "KQL式を使用して、アラートトリガーの範囲を制限します。", "xpack.infra.metrics.alertFlyout.filterLabel": "フィルター (任意) ", - "xpack.infra.metrics.alertFlyout.hourLabel": "時間", - "xpack.infra.metrics.alertFlyout.lastDayLabel": "昨日", - "xpack.infra.metrics.alertFlyout.lastHourLabel": "過去1時間", - "xpack.infra.metrics.alertFlyout.lastMonthLabel": "先月", - "xpack.infra.metrics.alertFlyout.lastWeekLabel": "先週", - "xpack.infra.metrics.alertFlyout.monthLabel": "月", "xpack.infra.metrics.alertFlyout.noDataHelpText": "有効にすると、メトリックが想定された期間内にデータを報告しない場合、またはアラートがElasticsearchをクエリできない場合に、アクションをトリガーします", "xpack.infra.metrics.alertFlyout.ofExpression.helpTextDetail": "メトリックが見つからない場合は、{documentationLink}。", "xpack.infra.metrics.alertFlyout.ofExpression.popoverLinkLabel": "データの追加方法", "xpack.infra.metrics.alertFlyout.outsideRangeLabel": "is not between", - "xpack.infra.metrics.alertFlyout.previewIntervalTooShortDescription": "選択するプレビュー長を長くするか、{checkEvery}フィールドの時間を増やしてください。", - "xpack.infra.metrics.alertFlyout.previewIntervalTooShortTitle": "データが不十分です", - "xpack.infra.metrics.alertFlyout.previewLabel": "プレビュー", "xpack.infra.metrics.alertFlyout.removeCondition": "条件を削除", "xpack.infra.metrics.alertFlyout.removeWarningThreshold": "warningThresholdを削除", - "xpack.infra.metrics.alertFlyout.testAlertCondition": "アラート条件のテスト", - "xpack.infra.metrics.alertFlyout.tooManyBucketsErrorDescription": "選択するプレビュー長を短くするか、{forTheLast}フィールドの時間を増やしてください。", - "xpack.infra.metrics.alertFlyout.tooManyBucketsErrorTitle": "データが多すぎます (>{maxBuckets}結果) ", "xpack.infra.metrics.alertFlyout.warningThreshold": "警告", - "xpack.infra.metrics.alertFlyout.weekLabel": "週", "xpack.infra.metrics.alerting.alertStateActionVariableDescription": "現在のアラートの状態", "xpack.infra.metrics.alerting.anomaly.defaultActionMessage": "\\{\\{alertName\\}\\}は\\{\\{context.alertState\\}\\}の状態です\n\n\\{\\{context.metric\\}\\}は\\{\\{context.timestamp\\}\\}で標準を超える\\{\\{context.summary\\}\\}でした\n\n標準の値:\\{\\{context.typical\\}\\}\n実際の値:\\{\\{context.actual\\}\\}\n", "xpack.infra.metrics.alerting.anomaly.fired": "実行", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d02e7f025e6e9..9c8c7a6357975 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12455,33 +12455,16 @@ "xpack.infra.metrics.alertFlyout.aggregationText.sum": "求和", "xpack.infra.metrics.alertFlyout.alertDescription": "当指标聚合超过阈值时告警。", "xpack.infra.metrics.alertFlyout.alertOnNoData": "没数据时提醒我", - "xpack.infra.metrics.alertFlyout.alertPreviewError": "尝试预览此告警条件时发生错误", - "xpack.infra.metrics.alertFlyout.alertPreviewErrorDesc": "请稍后重试或查看详情了解更多信息。", - "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "尝试评估部分数据时发生错误。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroupBy": "跨 {groups}", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups, plural,other {# 个 {groupName}}}", - "xpack.infra.metrics.alertFlyout.alertPreviewLookback": "在过去 {lookback}", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "有 {wereWas}{boldedResultsNumber}。", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, other {# 个无数据结果}}", - "xpack.infra.metrics.alertFlyout.alertPreviewOnlyOnStatusChange": "仅在状态更改时", - "xpack.infra.metrics.alertFlyout.alertPreviewResultInstances": "有 {wereWas}{firedTimes} 次满足此告警的条件", - "xpack.infra.metrics.alertFlyout.alertPreviewResultText": "{instanceCount} {groupByWithConditionalTrailingSpace}{lookbackText}。", - "xpack.infra.metrics.alertFlyout.alertPreviewResultWithSeverityLevels": "有 {wereWas}{criticalInstances}满足此告警的{boldCritical}条件,{warningInstances}满足{boldWarning}条件", - "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications": "因此,此告警将根据“{alertThrottle}”的选定“通知”设置发送{notifications}。", - "xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber": "{notifs, plural, other {# 个通知}}", "xpack.infra.metrics.alertFlyout.anomalyFilterHelpText": "将告警触发的范围限定在特定节点影响的异常。", "xpack.infra.metrics.alertFlyout.anomalyFilterHelpTextExample": "例如:“my-node-1”或“my-node-*”", "xpack.infra.metrics.alertFlyout.anomalyInfluencerFilterPlaceholder": "所有内容", "xpack.infra.metrics.alertFlyout.anomalyJobs.memoryUsage": "内存使用", "xpack.infra.metrics.alertFlyout.anomalyJobs.networkIn": "网络传入", "xpack.infra.metrics.alertFlyout.anomalyJobs.networkOut": "网络传出", - "xpack.infra.metrics.alertFlyout.boldCritical": "紧急", - "xpack.infra.metrics.alertFlyout.boldWarning": "警告", "xpack.infra.metrics.alertFlyout.conditions": "条件", "xpack.infra.metrics.alertFlyout.createAlertPerHelpText": "为每个唯一值创建告警。例如:“host.id”或“cloud.region”。", "xpack.infra.metrics.alertFlyout.createAlertPerText": "创建告警时间间隔(可选)", "xpack.infra.metrics.alertFlyout.criticalThreshold": "告警", - "xpack.infra.metrics.alertFlyout.dayLabel": "天", "xpack.infra.metrics.alertFlyout.error.aggregationRequired": "“聚合”必填。", "xpack.infra.metrics.alertFlyout.error.customMetricFieldRequired": "“字段”必填。", "xpack.infra.metrics.alertFlyout.error.metricRequired": "“指标”必填。", @@ -12489,7 +12472,6 @@ "xpack.infra.metrics.alertFlyout.error.thresholdRequired": "“阈值”必填。", "xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired": "阈值必须包含有效数字。", "xpack.infra.metrics.alertFlyout.error.timeRequred": "“时间大小”必填。", - "xpack.infra.metrics.alertFlyout.errorDetails": "详情", "xpack.infra.metrics.alertFlyout.expandRowLabel": "展开行。", "xpack.infra.metrics.alertFlyout.expression.for.descriptionLabel": "对于", "xpack.infra.metrics.alertFlyout.expression.for.popoverTitle": "节点类型", @@ -12505,28 +12487,13 @@ "xpack.infra.metrics.alertFlyout.filterByNodeLabel": "按节点筛选", "xpack.infra.metrics.alertFlyout.filterHelpText": "使用 KQL 表达式限制告警触发的范围。", "xpack.infra.metrics.alertFlyout.filterLabel": "筛选(可选)", - "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, other {# 个实例}}", - "xpack.infra.metrics.alertFlyout.hourLabel": "小时", - "xpack.infra.metrics.alertFlyout.lastDayLabel": "昨天", - "xpack.infra.metrics.alertFlyout.lastHourLabel": "上一小时", - "xpack.infra.metrics.alertFlyout.lastMonthLabel": "上个月", - "xpack.infra.metrics.alertFlyout.lastWeekLabel": "上周", - "xpack.infra.metrics.alertFlyout.monthLabel": "个月", "xpack.infra.metrics.alertFlyout.noDataHelpText": "启用此选项可在指标在预期的时间段中未报告任何数据时或告警无法查询 Elasticsearch 时触发操作", "xpack.infra.metrics.alertFlyout.ofExpression.helpTextDetail": "找不到指标?{documentationLink}。", "xpack.infra.metrics.alertFlyout.ofExpression.popoverLinkLabel": "了解如何添加更多数据", "xpack.infra.metrics.alertFlyout.outsideRangeLabel": "不介于", - "xpack.infra.metrics.alertFlyout.previewIntervalTooShortDescription": "尝试选择较长的预览长度或在“{checkEvery}”字段增大时间量。", - "xpack.infra.metrics.alertFlyout.previewIntervalTooShortTitle": "没有足够的数据", - "xpack.infra.metrics.alertFlyout.previewLabel": "预览", "xpack.infra.metrics.alertFlyout.removeCondition": "删除条件", "xpack.infra.metrics.alertFlyout.removeWarningThreshold": "移除警告阈值", - "xpack.infra.metrics.alertFlyout.testAlertCondition": "测试告警条件", - "xpack.infra.metrics.alertFlyout.tooManyBucketsErrorDescription": "尝试选择较短的预览长度或在“{forTheLast}”字段中增大时间量。", - "xpack.infra.metrics.alertFlyout.tooManyBucketsErrorTitle": "数据过多(>{maxBuckets} 个结果)", "xpack.infra.metrics.alertFlyout.warningThreshold": "警告", - "xpack.infra.metrics.alertFlyout.weekLabel": "周", - "xpack.infra.metrics.alertFlyout.wereWas": "{plurality, plural, other {}}", "xpack.infra.metrics.alerting.alertStateActionVariableDescription": "告警的当前状态", "xpack.infra.metrics.alerting.anomaly.defaultActionMessage": "\\{\\{alertName\\}\\} 处于 \\{\\{context.alertState\\}\\} 状态\n\n\\{\\{context.metric\\}\\} 在 \\{\\{context.timestamp\\}\\}比正常\\{\\{context.summary\\}\\}\n\n典型值:\\{\\{context.typical\\}\\}\n实际值:\\{\\{context.actual\\}\\}\n", "xpack.infra.metrics.alerting.anomaly.fired": "已触发", From acba612586149b8b08b016a62a0b1725f5784e67 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 12 Aug 2021 10:25:56 -0600 Subject: [PATCH 07/91] [Metrics UI] Move saved views button to page header (#107951) * [Metrics UI] Move saved views button to page header * fixing types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../saved_views/toolbar_control.tsx | 138 ++++++++---------- .../inventory_view/components/layout.tsx | 15 +- .../pages/metrics/inventory_view/index.tsx | 30 ++-- .../metrics_explorer/components/toolbar.tsx | 11 -- .../pages/metrics/metrics_explorer/index.tsx | 10 ++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../test/functional/apps/infra/home_page.ts | 2 +- .../functional/apps/infra/metrics_explorer.ts | 2 +- .../page_objects/infra_saved_views.ts | 5 +- 10 files changed, 90 insertions(+), 125 deletions(-) diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index ed46f0e555b99..9ed4047e45bd3 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -5,18 +5,9 @@ * 2.0. */ -import { EuiFlexGroup } from '@elastic/eui'; import React, { useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, -} from '@elastic/eui'; -import { EuiPopover, EuiLink } from '@elastic/eui'; -import { EuiListGroup, EuiListGroupItem } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiPopover, EuiListGroup, EuiListGroupItem } from '@elastic/eui'; import { SavedViewCreateModal } from './create_modal'; import { SavedViewUpdateModal } from './update_modal'; import { SavedViewManageViewsFlyout } from './manage_views_flyout'; @@ -147,80 +138,65 @@ export function SavedViewsToolbarControls(props: Props) { return ( <> - - - - - - - - - - {currentView - ? currentView.name - : i18n.translate('xpack.infra.savedView.unknownView', { - defaultMessage: 'No view selected', - })} - - - - - - } - isOpen={isSavedViewMenuOpen} - closePopover={hideSavedViewMenu} - anchorPosition="upCenter" - > - - + + {currentView + ? currentView.name + : i18n.translate('xpack.infra.savedView.unknownView', { + defaultMessage: 'No view selected', + })} + + } + isOpen={isSavedViewMenuOpen} + closePopover={hideSavedViewMenu} + anchorPosition="leftCenter" + > + + - + - + - - - - + + + {createModalOpen && ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 833b532a943d1..f241f5d118147 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import useInterval from 'react-use/lib/useInterval'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { SavedView } from '../../../../containers/saved_view/saved_view'; import { AutoSizer } from '../../../../components/auto_sizer'; import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; @@ -28,7 +28,6 @@ import { IntervalLabel } from './waffle/interval_label'; import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter'; import { createLegend } from '../lib/create_legend'; import { useWaffleViewState } from '../hooks/use_waffle_view_state'; -import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; import { BottomDrawer } from './bottom_drawer'; import { Legend } from './waffle/legend'; @@ -97,7 +96,7 @@ export const Layout = React.memo( const bounds = autoBounds ? dataBounds : boundsOverride; /* eslint-disable-next-line react-hooks/exhaustive-deps */ const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); - const { viewState, onViewChange } = useWaffleViewState(); + const { onViewChange } = useWaffleViewState(); useEffect(() => { if (currentView) { @@ -159,10 +158,6 @@ export const Layout = React.memo( - - - - {({ measureRef, bounds: { height = 0 } }) => ( @@ -221,9 +216,3 @@ const MainContainer = euiStyled.div` const TopActionContainer = euiStyled.div` padding: ${(props) => `12px ${props.theme.eui.paddingSizes.m}`}; `; - -const SavedViewContainer = euiStyled.div` - position: relative; - z-index: 1; - padding-left: ${(props) => props.theme.eui.paddingSizes.m}; -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 9671699dadbad..353997d5fe3ff 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -29,6 +29,7 @@ import { MetricsPageTemplate } from '../page_template'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { APP_WRAPPER_CLASS } from '../../../../../../../src/core/public'; import { inventoryTitle } from '../../../translations'; +import { SavedViews } from './components/saved_views'; export const SnapshotPage = () => { const uiCapabilities = useKibana().services.application?.capabilities; @@ -72,23 +73,24 @@ export const SnapshotPage = () => { ) : metricIndicesExist ? ( <> - - - ], + }} + pageBodyProps={{ + paddingSize: 'none', + }} > + - - + + ) : hasFailedLoadingSource ? ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx index 1b33546d3b68f..64da554dee690 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx @@ -23,7 +23,6 @@ import { MetricsExplorerMetrics } from './metrics'; import { MetricsExplorerGroupBy } from './group_by'; import { MetricsExplorerAggregationPicker } from './aggregation'; import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } from './chart_options'; -import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; @@ -114,16 +113,6 @@ export const MetricsExplorerToolbar = ({ chartOptions={chartOptions} /> - - - - , + ], }} > { const pageObjects = getPageObjects(['common', 'infraHome', 'infraSavedViews']); // Failing: See https://github.com/elastic/kibana/issues/106650 - describe.skip('Home page', function () { + describe('Home page', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); diff --git a/x-pack/test/functional/apps/infra/metrics_explorer.ts b/x-pack/test/functional/apps/infra/metrics_explorer.ts index 5a8d7da628259..90a875a07f8a7 100644 --- a/x-pack/test/functional/apps/infra/metrics_explorer.ts +++ b/x-pack/test/functional/apps/infra/metrics_explorer.ts @@ -88,7 +88,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); // FLAKY: https://github.com/elastic/kibana/issues/106651 - describe.skip('Saved Views', () => { + describe('Saved Views', () => { before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); describe('save functionality', () => { diff --git a/x-pack/test/functional/page_objects/infra_saved_views.ts b/x-pack/test/functional/page_objects/infra_saved_views.ts index 31b528d4ec153..ef91b3b5fcb3c 100644 --- a/x-pack/test/functional/page_objects/infra_saved_views.ts +++ b/x-pack/test/functional/page_objects/infra_saved_views.ts @@ -74,13 +74,14 @@ export function InfraSavedViewsProvider({ getService }: FtrProviderContext) { }, async ensureViewIsLoaded(name: string) { - const subject = await testSubjects.find('savedViews-currentViewName'); + const subject = await testSubjects.find('savedViews-openPopover'); expect(await subject.getVisibleText()).to.be(name); }, async ensureViewIsLoadable(name: string) { const subjects = await testSubjects.getVisibleTextAll('savedViews-loadList'); - expect(subjects.some((s) => s.split('\n').includes(name))).to.be(true); + const includesName = subjects.some((s) => s.includes(name)); + expect(includesName).to.be(true); }, async closeSavedViewsLoadModal() { From caaa76feab58c272cef61dd0a6833c6155a4d087 Mon Sep 17 00:00:00 2001 From: mgiota Date: Thu, 12 Aug 2021 18:39:32 +0200 Subject: [PATCH 08/91] [RAC] display timestamp value instead of triggered (#108029) * [RAC] display timestamp value instead of triggered * remove unused value * fix imports * fix imports * Update x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx Co-authored-by: Tiago Costa * add some explanations * more explanations * 108035: change relative time for timestamp to absolute Co-authored-by: Tiago Costa --- .../shared/timestamp_tooltip/index.test.tsx | 4 +-- .../shared/timestamp_tooltip/index.tsx | 5 +--- .../pages/alerts/alerts_table_t_grid.tsx | 27 +++++++++++-------- .../public/pages/alerts/render_cell_value.tsx | 13 +++++---- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/timestamp_tooltip/index.test.tsx b/x-pack/plugins/observability/public/components/shared/timestamp_tooltip/index.test.tsx index 14d0a64be5241..d08ad48a326af 100644 --- a/x-pack/plugins/observability/public/components/shared/timestamp_tooltip/index.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/timestamp_tooltip/index.test.tsx @@ -27,14 +27,14 @@ describe('TimestampTooltip', () => { afterAll(() => moment.tz.setDefault('')); - it('should render component with relative time in body and absolute time in tooltip', () => { + it('should render component with absolute time in body and absolute time in tooltip', () => { expect(shallow()).toMatchInlineSnapshot(` - 5 hours ago + Oct 10, 2019, 08:06:40.123 (UTC-7) `); }); diff --git a/x-pack/plugins/observability/public/components/shared/timestamp_tooltip/index.tsx b/x-pack/plugins/observability/public/components/shared/timestamp_tooltip/index.tsx index 784507fbfbcd8..7b82455ad5932 100644 --- a/x-pack/plugins/observability/public/components/shared/timestamp_tooltip/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/timestamp_tooltip/index.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; -import moment from 'moment-timezone'; import { asAbsoluteDateTime, TimeUnit } from '../../../../common/utils/formatters/datetime'; interface Props { @@ -19,13 +18,11 @@ interface Props { } export function TimestampTooltip({ time, timeUnit = 'milliseconds' }: Props) { - const momentTime = moment(time); - const relativeTimeLabel = momentTime.fromNow(); const absoluteTimeLabel = asAbsoluteDateTime(time, timeUnit); return ( - <>{relativeTimeLabel} + <>{absoluteTimeLabel} ); } diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index f82446db2ebec..2f287ee1d614d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -5,29 +5,35 @@ * 2.0. */ +/** + * We need to produce types and code transpilation at different folders during the build of the package. + * We have types and code at different imports because we don't want to import the whole package in the resulting webpack bundle for the plugin. + * This way plugins can do targeted imports to reduce the final code bundle + */ import type { AlertConsumers as AlertConsumersTyped, ALERT_DURATION as ALERT_DURATION_TYPED, ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, ALERT_STATUS as ALERT_STATUS_TYPED, - ALERT_START as ALERT_START_TYPED, ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, } from '@kbn/rule-data-utils'; import { - AlertConsumers as AlertConsumersNonTyped, ALERT_DURATION as ALERT_DURATION_NON_TYPED, ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, ALERT_STATUS as ALERT_STATUS_NON_TYPED, - ALERT_START as ALERT_START_NON_TYPED, ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, - // @ts-expect-error -} from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; + TIMESTAMP, + // @ts-expect-error importing from a place other than root because we want to limit what we import from this package +} from '@kbn/rule-data-utils/target_node/technical_field_names'; + +// @ts-expect-error importing from a place other than root because we want to limit what we import from this package +import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; + import { EuiButtonIcon, EuiDataGridColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import React, { Suspense, useState } from 'react'; - import type { TimelinesUIStart } from '../../../../timelines/public'; import type { TopAlert } from './'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; @@ -48,7 +54,6 @@ const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; -const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; interface AlertsTableTGridProps { @@ -96,11 +101,11 @@ export const columns: Array< }, { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.triggeredColumnDescription', { - defaultMessage: 'Triggered', + displayAsText: i18n.translate('xpack.observability.alertsTGrid.lastUpdatedColumnDescription', { + defaultMessage: 'Last updated', }), - id: ALERT_START, - initialWidth: 176, + id: TIMESTAMP, + initialWidth: 230, }, { columnHeaderType: 'not-filtered', diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index dde10b7a3f3e1..c080b6b94ed4a 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -7,20 +7,24 @@ import { EuiIconTip, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; +/** + * We need to produce types and code transpilation at different folders during the build of the package. + * We have types and code at different imports because we don't want to import the whole package in the resulting webpack bundle for the plugin. + * This way plugins can do targeted imports to reduce the final code bundle + */ import type { ALERT_DURATION as ALERT_DURATION_TYPED, ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, - ALERT_START as ALERT_START_TYPED, ALERT_STATUS as ALERT_STATUS_TYPED, ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, - ALERT_START as ALERT_START_NON_TYPED, ALERT_STATUS as ALERT_STATUS_NON_TYPED, ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, - // @ts-expect-error + TIMESTAMP, + // @ts-expect-error importing from a place other than root because we want to limit what we import from this package } from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common'; @@ -33,7 +37,6 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; -const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; @@ -101,7 +104,7 @@ export const getRenderCellValue = ({ type="check" /> ); - case ALERT_START: + case TIMESTAMP: return ; case ALERT_DURATION: return asDuration(Number(value)); From 4d7fd0a0ad8b33ba943a5958e86ca49b60ac2e8b Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Thu, 12 Aug 2021 12:39:58 -0400 Subject: [PATCH 09/91] [App Search] Added an EntryPointsTable (#108316) --- .../components/entry_points_table.test.tsx | 119 ++++++++++++++ .../crawler/components/entry_points_table.tsx | 150 ++++++++++++++++++ .../entry_points_table_logic.test.ts | 77 +++++++++ .../components/entry_points_table_logic.ts | 59 +++++++ .../crawler/crawler_single_domain.tsx | 14 +- .../crawler_single_domain_logic.test.ts | 30 ++++ .../crawler/crawler_single_domain_logic.ts | 6 +- ...generic_endpoint_inline_editable_table.tsx | 1 + .../index.ts | 8 + .../inline_editable_table.tsx | 2 + .../applications/shared/tables/types.ts | 2 +- .../app_search/crawler_entry_points.test.ts | 134 ++++++++++++++++ .../routes/app_search/crawler_entry_points.ts | 79 +++++++++ .../server/routes/app_search/index.ts | 2 + 14 files changed, 679 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/index.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.test.tsx new file mode 100644 index 0000000000000..09f1523f8e3be --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.test.tsx @@ -0,0 +1,119 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { EuiFieldText } from '@elastic/eui'; + +import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; + +import { mountWithIntl } from '../../../../test_helpers'; + +import { EntryPointsTable } from './entry_points_table'; + +describe('EntryPointsTable', () => { + const engineName = 'my-engine'; + const entryPoints = [ + { id: '1', value: '/whatever' }, + { id: '2', value: '/foo' }, + ]; + const domain = { + createdOn: '2018-01-01T00:00:00.000Z', + documentCount: 10, + id: '6113e1407a2f2e6f42489794', + url: 'https://www.elastic.co', + crawlRules: [], + entryPoints, + sitemaps: [], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(GenericEndpointInlineEditableTable).exists()).toBe(true); + }); + + describe('the first and only column in the table', () => { + it('shows the value of an entry point', () => { + const entryPoint = { id: '1', value: '/whatever' }; + + const wrapper = shallow( + + ); + + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + const column = shallow(

{columns[0].render(entryPoint)}
); + expect(column.html()).toContain('/whatever'); + }); + + it('can show the value of an entry point as editable', () => { + const entryPoint = { id: '1', value: '/whatever' }; + const onChange = jest.fn(); + + const wrapper = shallow( + + ); + + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + const column = shallow( +
+ {columns[0].editingRender(entryPoint, onChange, { isInvalid: false, isLoading: false })} +
+ ); + + const textField = column.find(EuiFieldText); + expect(textField.props()).toEqual( + expect.objectContaining({ + value: '/whatever', + disabled: false, // It would be disabled if isLoading is true + isInvalid: false, + prepend: 'https://www.elastic.co', + }) + ); + + textField.simulate('change', { target: { value: '/foo' } }); + expect(onChange).toHaveBeenCalledWith('/foo'); + }); + }); + + describe('routes', () => { + it('can calculate an update and delete route correctly', () => { + const wrapper = shallow( + + ); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const entryPoint = { id: '1', value: '/whatever' }; + expect(table.prop('deleteRoute')(entryPoint)).toEqual( + '/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/entry_points/1' + ); + expect(table.prop('updateRoute')(entryPoint)).toEqual( + '/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/entry_points/1' + ); + }); + }); + + it('shows a no items message whem there are no entry points to show', () => { + const wrapper = shallow( + + ); + + const editNewItems = jest.fn(); + const table = wrapper.find(GenericEndpointInlineEditableTable); + const message = mountWithIntl(
{table.prop('noItemsMessage')!(editNewItems)}
); + expect(message.html()).toContain('There are no existing entry points.'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx new file mode 100644 index 0000000000000..7657a23ce4789 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx @@ -0,0 +1,150 @@ +/* + * 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 { useActions } from 'kea'; + +import { EuiFieldText, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; +import { InlineEditableTableColumn } from '../../../../shared/tables/inline_editable_table/types'; +import { ItemWithAnID } from '../../../../shared/tables/types'; +import { DOCS_PREFIX } from '../../../routes'; +import { CrawlerDomain, EntryPoint } from '../types'; + +import { EntryPointsTableLogic } from './entry_points_table_logic'; + +interface EntryPointsTableProps { + domain: CrawlerDomain; + engineName: string; + items: EntryPoint[]; +} + +export const EntryPointsTable: React.FC = ({ + domain, + engineName, + items, +}) => { + const { onAdd, onDelete, onUpdate } = useActions(EntryPointsTableLogic); + const field = 'value'; + + const columns: Array> = [ + { + editingRender: (entryPoint, onChange, { isInvalid, isLoading }) => ( + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + prepend={domain.url} + /> + ), + render: (entryPoint) => ( + + {domain.url} + {(entryPoint as EntryPoint)[field]} + + ), + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.urlTableHead', + { defaultMessage: 'URL' } + ), + field, + }, + ]; + + const entryPointsRoute = `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}/entry_points`; + + const getEntryPointRoute = (entryPoint: EntryPoint) => + `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}/entry_points/${entryPoint.id}`; + + return ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.description', { + defaultMessage: + 'Include the most important URLs for your website here. Entry point URLs will be the first pages to be indexed and processed for links to other pages.', + })}{' '} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.learnMoreLinkText', + { defaultMessage: 'Learn more about entry points.' } + )} + +

+ } + instanceId="EntryPointsTable" + items={items} + lastItemWarning={i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.lastItemMessage', + { defaultMessage: 'The crawler requires at least one entry point.' } + )} + // Since canRemoveLastItem is false, the only time noItemsMessage would be displayed is if the last entry point was deleted via the API. + noItemsMessage={(editNewItem) => ( + <> + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.emptyMessageTitle', + { + defaultMessage: 'There are no existing entry points.', + } + )} +

+
+ + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.emptyMessageLinkText', + { defaultMessage: 'Add an entry point' } + )} + + ), + }} + /> + + + + )} + addRoute={entryPointsRoute} + canRemoveLastItem={false} + deleteRoute={getEntryPointRoute} + updateRoute={getEntryPointRoute} + dataProperty="entry_points" + onAdd={onAdd} + onDelete={onDelete} + onUpdate={onUpdate} + title={i18n.translate('xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.title', { + defaultMessage: 'Entry points', + })} + disableReordering + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.test.ts new file mode 100644 index 0000000000000..7d6704b9abdb3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.test.ts @@ -0,0 +1,77 @@ +/* + * 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. + */ + +jest.mock('../crawler_single_domain_logic', () => ({ + CrawlerSingleDomainLogic: { + actions: { + updateEntryPoints: jest.fn(), + }, + }, +})); + +import { LogicMounter, mockFlashMessageHelpers } from '../../../../__mocks__/kea_logic'; + +import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic'; + +import { EntryPointsTableLogic } from './entry_points_table_logic'; + +describe('EntryPointsTableLogic', () => { + const { mount } = new LogicMounter(EntryPointsTableLogic); + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('listeners', () => { + describe('onAdd', () => { + it('should update the entry points for the current domain, and clear flash messages', () => { + const entryThatWasAdded = { id: '2', value: 'bar' }; + const updatedEntries = [ + { id: '1', value: 'foo' }, + { id: '2', value: 'bar' }, + ]; + mount(); + EntryPointsTableLogic.actions.onAdd(entryThatWasAdded, updatedEntries); + expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith( + updatedEntries + ); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('onDelete', () => { + it('should update the entry points for the current domain, clear flash messages, and show a success toast', () => { + const entryThatWasDeleted = { id: '2', value: 'bar' }; + const updatedEntries = [{ id: '1', value: 'foo' }]; + mount(); + EntryPointsTableLogic.actions.onDelete(entryThatWasDeleted, updatedEntries); + expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith( + updatedEntries + ); + expect(clearFlashMessages).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); + }); + }); + + describe('onUpdate', () => { + it('should update the entry points for the current domain, clear flash messages, and show a success toast', () => { + const entryThatWasUpdated = { id: '2', value: 'baz' }; + const updatedEntries = [ + { id: '1', value: 'foo' }, + { id: '2', value: 'baz' }, + ]; + mount(); + EntryPointsTableLogic.actions.onUpdate(entryThatWasUpdated, updatedEntries); + expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith( + updatedEntries + ); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.ts new file mode 100644 index 0000000000000..2332a24ea8b74 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { clearFlashMessages, flashSuccessToast } from '../../../../shared/flash_messages'; + +import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic'; + +import { EntryPoint } from '../types'; + +interface EntryPointsTableValues { + dataLoading: boolean; +} + +interface EntryPointsTableActions { + onAdd( + entryPoint: EntryPoint, + entryPoints: EntryPoint[] + ): { entryPoint: EntryPoint; entryPoints: EntryPoint[] }; + onDelete( + entryPoint: EntryPoint, + entryPoints: EntryPoint[] + ): { entryPoint: EntryPoint; entryPoints: EntryPoint[] }; + onUpdate( + entryPoint: EntryPoint, + entryPoints: EntryPoint[] + ): { entryPoint: EntryPoint; entryPoints: EntryPoint[] }; +} + +export const EntryPointsTableLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'entry_points_table'], + actions: () => ({ + onAdd: (entryPoint, entryPoints) => ({ entryPoint, entryPoints }), + onDelete: (entryPoint, entryPoints) => ({ entryPoint, entryPoints }), + onUpdate: (entryPoint, entryPoints) => ({ entryPoint, entryPoints }), + }), + listeners: () => ({ + onAdd: ({ entryPoints }) => { + CrawlerSingleDomainLogic.actions.updateEntryPoints(entryPoints); + clearFlashMessages(); + }, + onDelete: ({ entryPoint, entryPoints }) => { + CrawlerSingleDomainLogic.actions.updateEntryPoints(entryPoints); + clearFlashMessages(); + flashSuccessToast(`Entry point "${entryPoint.value}" was removed.`); + }, + onUpdate: ({ entryPoints }) => { + CrawlerSingleDomainLogic.actions.updateEntryPoints(entryPoints); + clearFlashMessages(); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index 6419c31cc16ca..da910ebc30726 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -11,22 +11,24 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiCode, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiCode, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getEngineBreadcrumbs } from '../engine'; +import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeleteDomainPanel } from './components/delete_domain_panel'; +import { EntryPointsTable } from './components/entry_points_table'; import { ManageCrawlsPopover } from './components/manage_crawls_popover/manage_crawls_popover'; import { CRAWLER_TITLE } from './constants'; import { CrawlerSingleDomainLogic } from './crawler_single_domain_logic'; export const CrawlerSingleDomain: React.FC = () => { const { domainId } = useParams() as { domainId: string }; + const { engineName } = EngineLogic.values; const { dataLoading, domain } = useValues(CrawlerSingleDomainLogic); @@ -51,6 +53,14 @@ export const CrawlerSingleDomain: React.FC = () => { > + {domain && ( + <> + + + + + + )}

{i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index f6c3b2c87ab8c..ead0c0ad91ced 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -51,6 +51,36 @@ describe('CrawlerSingleDomainLogic', () => { expect(CrawlerSingleDomainLogic.values.domain).toEqual(domain); }); }); + + describe('updateEntryPoints', () => { + beforeEach(() => { + mount({ + domain: { + id: '507f1f77bcf86cd799439011', + entryPoints: [], + }, + }); + + CrawlerSingleDomainLogic.actions.updateEntryPoints([ + { + id: '1234', + value: '/', + }, + ]); + }); + + it('should update the entry points on the domain', () => { + expect(CrawlerSingleDomainLogic.values.domain).toEqual({ + id: '507f1f77bcf86cd799439011', + entryPoints: [ + { + id: '1234', + value: '/', + }, + ], + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts index 7b5ba6984f106..780cab45564bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts @@ -14,7 +14,7 @@ import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_CRAWLER_PATH } from '../../routes'; import { EngineLogic, generateEnginePath } from '../engine'; -import { CrawlerDomain } from './types'; +import { CrawlerDomain, EntryPoint } from './types'; import { crawlerDomainServerToClient, getDeleteDomainSuccessMessage } from './utils'; export interface CrawlerSingleDomainValues { @@ -26,6 +26,7 @@ interface CrawlerSingleDomainActions { deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain }; fetchDomainData(domainId: string): { domainId: string }; onReceiveDomainData(domain: CrawlerDomain): { domain: CrawlerDomain }; + updateEntryPoints(entryPoints: EntryPoint[]): { entryPoints: EntryPoint[] }; } export const CrawlerSingleDomainLogic = kea< @@ -36,6 +37,7 @@ export const CrawlerSingleDomainLogic = kea< deleteDomain: (domain) => ({ domain }), fetchDomainData: (domainId) => ({ domainId }), onReceiveDomainData: (domain) => ({ domain }), + updateEntryPoints: (entryPoints) => ({ entryPoints }), }, reducers: { dataLoading: [ @@ -48,6 +50,8 @@ export const CrawlerSingleDomainLogic = kea< null, { onReceiveDomainData: (_, { domain }) => domain, + updateEntryPoints: (currentDomain, { entryPoints }) => + ({ ...currentDomain, entryPoints } as CrawlerDomain), }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table.tsx index 63b3d4b407667..4db5e81653a9a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table.tsx @@ -32,6 +32,7 @@ export interface GenericEndpointInlineEditableTableProps onDelete(item: ItemWithAnID, items: ItemWithAnID[]): void; onUpdate(item: ItemWithAnID, items: ItemWithAnID[]): void; onReorder?(items: ItemWithAnID[]): void; + disableReordering?: boolean; } export const GenericEndpointInlineEditableTable = ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/index.ts new file mode 100644 index 0000000000000..ce766171a85c6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/index.ts @@ -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 { GenericEndpointInlineEditableTable } from './generic_endpoint_inline_editable_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx index 3e351f5826abc..093692dfde335 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx @@ -33,6 +33,7 @@ export interface InlineEditableTableProps { canRemoveLastItem?: boolean; className?: string; description?: React.ReactNode; + disableReordering?: boolean; isLoading?: boolean; lastItemWarning?: string; noItemsMessage?: (editNewItem: () => void) => React.ReactNode; @@ -170,6 +171,7 @@ export const InlineEditableTableContents = ({ noItemsMessage={noItemsMessage(editNewItem)} onReorder={reorderItems} disableDragging={isEditing} + {...rest} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/types.ts index f2a2531726b33..8407a14eae207 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/types.ts @@ -6,6 +6,6 @@ */ export type ItemWithAnID = { - id: number | null; + id: number | string | null; created_at?: string; } & object; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts new file mode 100644 index 0000000000000..cdfb397b47c7e --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts @@ -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 { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; + +import { registerCrawlerEntryPointRoutes } from './crawler_entry_points'; + +describe('crawler entry point routes', () => { + describe('POST /api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', + }); + + registerCrawlerEntryPointRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234' }, + body: { + value: 'test', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + describe('PUT /api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', + }); + + registerCrawlerEntryPointRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234', entryPointId: '5678' }, + body: { + value: 'test', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + describe('DELETE /api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', + }); + + registerCrawlerEntryPointRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234', entryPointId: '5678' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts new file mode 100644 index 0000000000000..88cb58f70953c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerCrawlerEntryPointRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + }), + body: schema.object({ + value: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points', + params: { + respond_with: 'index', + }, + }) + ); + + router.put( + { + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + entryPointId: schema.string(), + }), + body: schema.object({ + value: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + params: { + respond_with: 'index', + }, + }) + ); + + router.delete( + { + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + entryPointId: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + params: { + respond_with: 'index', + }, + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 2442b61c632c1..af5cc78f01e78 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -10,6 +10,7 @@ import { RouteDependencies } from '../../plugin'; import { registerAnalyticsRoutes } from './analytics'; import { registerApiLogsRoutes } from './api_logs'; import { registerCrawlerRoutes } from './crawler'; +import { registerCrawlerEntryPointRoutes } from './crawler_entry_points'; import { registerCredentialsRoutes } from './credentials'; import { registerCurationsRoutes } from './curations'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; @@ -44,4 +45,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerApiLogsRoutes(dependencies); registerOnboardingRoutes(dependencies); registerCrawlerRoutes(dependencies); + registerCrawlerEntryPointRoutes(dependencies); }; From f08005e0e7d93fa2ed28ba768e43ceb535c06768 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 12 Aug 2021 18:40:19 +0200 Subject: [PATCH 10/91] [Reporting] Create reports with full state required to generate the report (#101048) * very wip * - Reached first iteration of reporting body value being saved with the report for **PDF** - Removed v2 of the reporting since it looks like we may be able to make a backwards compatible change on existing PDF/PNG exports * reintroduced pdfv2 export type, see https://github.com/elastic/kibana/issues/99890\#issuecomment-851527878 * fix a whol bunch of imports * mapped out a working version for pdf * refactor to tuples * added v2 pdf to export type registry * a lot of hackery to get reports generated in v2 * added png v2, png reports with locator state * wip: refactored for loading the saved object on the redirect app URL * major wip: initial stages of reporting redirect app, need to add a way to generate v2 reports! * added a way to generate a v2 pdf from the example reporting plugin * updated reporting example app to read and accept forwarded app state * added reporting locator and updated server-side route to not use Boom * removed reporting locator for now, first iteration of reports being generated using the reporting redirect app * version with PNG working * moved png/v2 -> png_v2 * moved printable_pdf/v2 -> printable_pdf_v2 * updated share public setup and start mocks * fix types after merging master * locator -> locatorParams AND added a new endpoint for getting locator params to client * fix type import * fix types * clean up bad imports * forceNow required on v2 payloads * reworked create job interface for PNG task payload and updated consumer code report example for forcenow * put locatorparams[] back onto the reportsource interface because on baseparams it conflicts with the different export type params * move getCustomLogo and generatePng to common for export types * additional import fixes * urls -> url * chore: fix and update types and fix jest import mocks * - refactored v2 behaviour to avoid client-side request for locator instead this value is injected pre-page-load so that the redirect app can use it - refactored the interface for the getScreenshot observable factory. specifically we now expect 'urlsOrUrlTuples' to be passed in. tested with new and old report types. * updated the reporting example app to use locator migration for v2 report types * added functionality for setting forceNow * added forceNow to job payload for v2 report types and fixed shared components for v2 * write the output of v2 reports to stream * fix types for forceNow * added tests for execute job * added comments, organized imports, removed selectors from report params * fix some type issues * feedback: removed duplicated PDF code, cleaned screenshot observable function and other minor tweaks * use variable (not destructured values) and remove unused import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .eslintignore | 1 + src/plugins/screenshot_mode/server/types.ts | 3 +- src/plugins/share/public/mocks.ts | 2 + src/plugins/share/public/plugin.ts | 22 ++- .../url_service/redirect/redirect_manager.ts | 4 + .../reporting_example/common/index.ts | 6 + .../reporting_example/common/locator.ts | 30 ++++ x-pack/examples/reporting_example/kibana.json | 2 +- .../reporting_example/public/application.tsx | 15 +- .../public/components/app.tsx | 117 +++++++++++- .../reporting_example/public/plugin.ts | 15 +- .../reporting_example/public/types.ts | 4 + x-pack/plugins/reporting/common/constants.ts | 18 ++ x-pack/plugins/reporting/common/job_utils.ts | 11 ++ x-pack/plugins/reporting/common/types.ts | 20 +++ x-pack/plugins/reporting/public/constants.ts | 8 + .../lib/reporting_api_client/context.tsx | 2 +- .../public/lib/reporting_api_client/index.ts | 2 +- .../management/mount_management_section.tsx | 6 +- .../public/management/report_listing.test.tsx | 6 +- x-pack/plugins/reporting/public/plugin.ts | 10 ++ .../reporting/public/redirect/index.ts | 8 + .../public/redirect/mount_redirect_app.tsx | 33 ++++ .../public/redirect/redirect_app.tsx | 74 ++++++++ .../public/share_context_menu/index.ts | 1 + .../register_pdf_png_reporting.tsx | 35 +++- .../reporting_panel_content.tsx | 10 +- .../public/shared/get_shared_components.tsx | 32 +++- x-pack/plugins/reporting/public/utils.ts | 12 ++ .../chromium/driver/chromium_driver.ts | 27 ++- .../{png/lib => common}/generate_png.ts | 15 +- .../lib => common}/get_custom_logo.test.ts | 6 +- .../lib => common}/get_custom_logo.ts | 8 +- .../server/export_types/common/index.ts | 3 + .../lib => common}/pdf/get_doc_options.ts | 0 .../lib => common}/pdf/get_font.test.ts | 0 .../lib => common}/pdf/get_font.ts | 0 .../lib => common}/pdf/get_template.ts | 2 +- .../lib => common}/pdf/index.test.ts | 4 +- .../lib => common}/pdf/index.ts | 4 +- .../export_types/common/set_force_now.ts | 22 +++ .../export_types/common/v2/get_full_urls.ts | 34 ++++ .../png/execute_job/index.test.ts | 4 +- .../export_types/png/execute_job/index.ts | 2 +- .../server/export_types/png_v2/create_job.ts | 29 +++ .../export_types/png_v2/execute_job.test.ts | 167 ++++++++++++++++++ .../server/export_types/png_v2/execute_job.ts | 74 ++++++++ .../server/export_types/png_v2/index.ts | 39 ++++ .../server/export_types/png_v2/metadata.ts | 13 ++ .../server/export_types/png_v2/types.d.ts | 29 +++ .../printable_pdf/execute_job/index.ts | 2 +- .../printable_pdf/lib/generate_pdf.ts | 4 +- .../export_types/printable_pdf/types.d.ts | 1 + .../printable_pdf_v2/create_job.ts | 28 +++ .../printable_pdf_v2/execute_job.test.ts | 126 +++++++++++++ .../printable_pdf_v2/execute_job.ts | 88 +++++++++ .../export_types/printable_pdf_v2/index.ts | 39 ++++ .../printable_pdf_v2/lib/generate_pdf.ts | 130 ++++++++++++++ .../printable_pdf_v2/lib/tracker.ts | 88 +++++++++ .../printable_pdf_v2/lib/uri_encode.js | 32 ++++ .../export_types/printable_pdf_v2/metadata.ts | 11 ++ .../export_types/printable_pdf_v2/types.ts | 31 ++++ x-pack/plugins/reporting/server/index.ts | 1 - .../server/lib/export_types_registry.ts | 5 + .../reporting/server/lib/screenshots/index.ts | 3 +- .../server/lib/screenshots/observable.test.ts | 13 +- .../server/lib/screenshots/observable.ts | 16 +- .../server/lib/screenshots/open_url.ts | 20 ++- x-pack/plugins/reporting/server/plugin.ts | 8 +- .../routes/diagnostic/screenshot.test.ts | 4 +- .../server/routes/diagnostic/screenshot.ts | 3 +- .../plugins/reporting/server/routes/jobs.ts | 2 +- x-pack/plugins/reporting/server/types.ts | 2 +- 73 files changed, 1553 insertions(+), 95 deletions(-) create mode 100644 x-pack/examples/reporting_example/common/locator.ts create mode 100644 x-pack/plugins/reporting/common/job_utils.ts create mode 100644 x-pack/plugins/reporting/public/constants.ts create mode 100644 x-pack/plugins/reporting/public/redirect/index.ts create mode 100644 x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx create mode 100644 x-pack/plugins/reporting/public/redirect/redirect_app.tsx create mode 100644 x-pack/plugins/reporting/public/utils.ts rename x-pack/plugins/reporting/server/export_types/{png/lib => common}/generate_png.ts (85%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib => common}/get_custom_logo.test.ts (91%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib => common}/get_custom_logo.ts (78%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib => common}/pdf/get_doc_options.ts (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib => common}/pdf/get_font.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib => common}/pdf/get_font.ts (100%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib => common}/pdf/get_template.ts (98%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib => common}/pdf/index.test.ts (97%) rename x-pack/plugins/reporting/server/export_types/{printable_pdf/lib => common}/pdf/index.ts (96%) create mode 100644 x-pack/plugins/reporting/server/export_types/common/set_force_now.ts create mode 100644 x-pack/plugins/reporting/server/export_types/common/v2/get_full_urls.ts create mode 100644 x-pack/plugins/reporting/server/export_types/png_v2/create_job.ts create mode 100644 x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts create mode 100644 x-pack/plugins/reporting/server/export_types/png_v2/index.ts create mode 100644 x-pack/plugins/reporting/server/export_types/png_v2/metadata.ts create mode 100644 x-pack/plugins/reporting/server/export_types/png_v2/types.d.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/create_job.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/index.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/uri_encode.js create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/metadata.ts create mode 100644 x-pack/plugins/reporting/server/export_types/printable_pdf_v2/types.ts diff --git a/.eslintignore b/.eslintignore index f757ed9a1bf98..66684fbcd52e6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -27,6 +27,7 @@ snapshots.js /x-pack/plugins/canvas/shareable_runtime/build /x-pack/plugins/canvas/storybook/build /x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** +/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/server/lib/pdf/assets/** # package overrides /packages/elastic-eslint-config-kibana diff --git a/src/plugins/screenshot_mode/server/types.ts b/src/plugins/screenshot_mode/server/types.ts index 4347252e58fce..566ae19719454 100644 --- a/src/plugins/screenshot_mode/server/types.ts +++ b/src/plugins/screenshot_mode/server/types.ts @@ -19,7 +19,8 @@ export interface ScreenshotModePluginSetup { isScreenshotMode: IsScreenshotMode; /** - * Set the current environment to screenshot mode. Intended to run in a browser-environment. + * Set the current environment to screenshot mode. Intended to run in a browser-environment, before any other scripts + * on the page have run to ensure that screenshot mode is detected as early as possible. */ setScreenshotModeEnabled: () => void; } diff --git a/src/plugins/share/public/mocks.ts b/src/plugins/share/public/mocks.ts index 3333878676e20..d72068afa285e 100644 --- a/src/plugins/share/public/mocks.ts +++ b/src/plugins/share/public/mocks.ts @@ -27,6 +27,7 @@ const createSetupContract = (): Setup => { registerUrlGenerator: jest.fn(), }, url, + navigate: jest.fn(), }; return setupContract; }; @@ -38,6 +39,7 @@ const createStartContract = (): Start => { getUrlGenerator: jest.fn(), }, toggleShareContextMenu: jest.fn(), + navigate: jest.fn(), }; return startContract; }; diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index adc28556d7a3c..1382e1968192c 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -19,7 +19,7 @@ import { UrlGeneratorsStart, } from './url_generators/url_generator_service'; import { UrlService } from '../common/url_service'; -import { RedirectManager } from './url_service'; +import { RedirectManager, RedirectOptions } from './url_service'; export interface ShareSetupDependencies { securityOss?: SecurityOssPluginSetup; @@ -42,6 +42,12 @@ export type SharePluginSetup = ShareMenuRegistrySetup & { * Utilities to work with URL locators and short URLs. */ url: UrlService; + + /** + * Accepts serialized values for extracting a locator, migrating state from a provided version against + * the locator, then using the locator to navigate. + */ + navigate(options: RedirectOptions): void; }; /** @public */ @@ -57,12 +63,20 @@ export type SharePluginStart = ShareMenuManagerStart & { * Utilities to work with URL locators and short URLs. */ url: UrlService; + + /** + * Accepts serialized values for extracting a locator, migrating state from a provided version against + * the locator, then using the locator to navigate. + */ + navigate(options: RedirectOptions): void; }; export class SharePlugin implements Plugin { private readonly shareMenuRegistry = new ShareMenuRegistry(); private readonly shareContextMenu = new ShareMenuManager(); private readonly urlGeneratorsService = new UrlGeneratorsService(); + + private redirectManager?: RedirectManager; private url?: UrlService; public setup(core: CoreSetup, plugins: ShareSetupDependencies): SharePluginSetup { @@ -87,15 +101,16 @@ export class SharePlugin implements Plugin { }, }); - const redirectManager = new RedirectManager({ + this.redirectManager = new RedirectManager({ url: this.url, }); - redirectManager.registerRedirectApp(core); + this.redirectManager.registerRedirectApp(core); return { ...this.shareMenuRegistry.setup(), urlGenerators: this.urlGeneratorsService.setup(core), url: this.url, + navigate: (options: RedirectOptions) => this.redirectManager!.navigate(options), }; } @@ -108,6 +123,7 @@ export class SharePlugin implements Plugin { ), urlGenerators: this.urlGeneratorsService.start(core), url: this.url!, + navigate: (options: RedirectOptions) => this.redirectManager!.navigate(options), }; } } diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts index 494fb623a48af..cc45e0d3126af 100644 --- a/src/plugins/share/public/url_service/redirect/redirect_manager.ts +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts @@ -52,6 +52,10 @@ export class RedirectManager { public onMount(urlLocationSearch: string) { const options = this.parseSearchParams(urlLocationSearch); + this.navigate(options); + } + + public navigate(options: RedirectOptions) { const locator = this.deps.url.locators.get(options.id); if (!locator) { diff --git a/x-pack/examples/reporting_example/common/index.ts b/x-pack/examples/reporting_example/common/index.ts index f01f2673eff56..ba2fcd21c8c70 100644 --- a/x-pack/examples/reporting_example/common/index.ts +++ b/x-pack/examples/reporting_example/common/index.ts @@ -7,3 +7,9 @@ export const PLUGIN_ID = 'reportingExample'; export const PLUGIN_NAME = 'reportingExample'; + +export { + REPORTING_EXAMPLE_LOCATOR_ID, + ReportingExampleLocatorDefinition, + ReportingExampleLocatorParams, +} from './locator'; diff --git a/x-pack/examples/reporting_example/common/locator.ts b/x-pack/examples/reporting_example/common/locator.ts new file mode 100644 index 0000000000000..fc39ec1c52654 --- /dev/null +++ b/x-pack/examples/reporting_example/common/locator.ts @@ -0,0 +1,30 @@ +/* + * 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 { SerializableRecord } from '@kbn/utility-types'; +import type { LocatorDefinition } from '../../../../src/plugins/share/public'; +import { PLUGIN_ID } from '../common'; + +export const REPORTING_EXAMPLE_LOCATOR_ID = 'REPORTING_EXAMPLE_LOCATOR_ID'; + +export type ReportingExampleLocatorParams = SerializableRecord; + +export class ReportingExampleLocatorDefinition implements LocatorDefinition<{}> { + public readonly id = REPORTING_EXAMPLE_LOCATOR_ID; + + migrations = { + '1.0.0': (state: {}) => ({ ...state, migrated: true }), + }; + + public readonly getLocation = async (params: {}) => { + return { + app: PLUGIN_ID, + path: '/', + state: params, + }; + }; +} diff --git a/x-pack/examples/reporting_example/kibana.json b/x-pack/examples/reporting_example/kibana.json index 64f0fcd62a220..716c6ea29c2a0 100644 --- a/x-pack/examples/reporting_example/kibana.json +++ b/x-pack/examples/reporting_example/kibana.json @@ -10,5 +10,5 @@ }, "description": "Example integration code for applications to feature reports.", "optionalPlugins": [], - "requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode"] + "requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode", "share"] } diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx index 5e72a9bd8fbbc..d945048ecd73e 100644 --- a/x-pack/examples/reporting_example/public/application.tsx +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -9,14 +9,23 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; import { ReportingExampleApp } from './components/app'; -import { SetupDeps, StartDeps } from './types'; +import { SetupDeps, StartDeps, MyForwardableState } from './types'; export const renderApp = ( coreStart: CoreStart, deps: Omit, - { appBasePath, element }: AppMountParameters // FIXME: appBasePath is deprecated + { appBasePath, element }: AppMountParameters, // FIXME: appBasePath is deprecated + forwardedParams: MyForwardableState ) => { - ReactDOM.render(, element); + ReactDOM.render( + , + element + ); return () => ReactDOM.unmountComponentAtNode(element); }; diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/components/app.tsx index a29c5d2e018d0..a34cd0ab518de 100644 --- a/x-pack/examples/reporting_example/public/components/app.tsx +++ b/x-pack/examples/reporting_example/public/components/app.tsx @@ -21,7 +21,10 @@ import { EuiPopover, EuiText, EuiTitle, + EuiCodeBlock, + EuiSpacer, } from '@elastic/eui'; +import moment from 'moment'; import { I18nProvider } from '@kbn/i18n/react'; import React, { useEffect, useState } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -29,11 +32,18 @@ import * as Rx from 'rxjs'; import { takeWhile } from 'rxjs/operators'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; import { constants, ReportingStart } from '../../../../../x-pack/plugins/reporting/public'; +import type { JobParamsPDFV2 } from '../../../../plugins/reporting/server/export_types/printable_pdf_v2/types'; +import type { JobParamsPNGV2 } from '../../../../plugins/reporting/server/export_types/png_v2/types'; + +import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common'; + +import { MyForwardableState } from '../types'; interface ReportingExampleAppProps { basename: string; reporting: ReportingStart; screenshotMode: ScreenshotModePluginSetup; + forwardedParams?: MyForwardableState; } const sourceLogos = ['Beats', 'Cloud', 'Logging', 'Kibana']; @@ -42,8 +52,12 @@ export const ReportingExampleApp = ({ basename, reporting, screenshotMode, + forwardedParams, }: ReportingExampleAppProps) => { - const { getDefaultLayoutSelectors } = reporting; + useEffect(() => { + // eslint-disable-next-line no-console + console.log('forwardedParams', forwardedParams); + }, [forwardedParams]); // Context Menu const [isPopoverOpen, setPopover] = useState(false); @@ -70,7 +84,6 @@ export const ReportingExampleApp = ({ return { layout: { id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, - selectors: getDefaultLayoutSelectors(), }, relativeUrls: ['/app/reportingExample#/intended-visualization'], objectType: 'develeloperExample', @@ -78,20 +91,65 @@ export const ReportingExampleApp = ({ }; }; + const getPDFJobParamsDefaultV2 = (): JobParamsPDFV2 => { + return { + version: '8.0.0', + layout: { + id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + }, + locatorParams: [ + { id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', params: { myTestState: {} } }, + ], + objectType: 'develeloperExample', + title: 'Reporting Developer Example', + browserTimezone: moment.tz.guess(), + }; + }; + + const getPNGJobParamsDefaultV2 = (): JobParamsPNGV2 => { + return { + version: '8.0.0', + layout: { + id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + }, + locatorParams: { + id: REPORTING_EXAMPLE_LOCATOR_ID, + version: '0.5.0', + params: { myTestState: {} }, + }, + objectType: 'develeloperExample', + title: 'Reporting Developer Example', + browserTimezone: moment.tz.guess(), + }; + }; + const panels = [ - { id: 0, items: [{ name: 'PDF Reports', icon: 'document', panel: 1 }] }, + { + id: 0, + items: [ + { name: 'PDF Reports', icon: 'document', panel: 1 }, + { name: 'PNG Reports', icon: 'document', panel: 7 }, + ], + }, { id: 1, initialFocusedItemIndex: 1, title: 'PDF Reports', items: [ - { name: 'No Layout Option', icon: 'document', panel: 2 }, + { name: 'Default layout', icon: 'document', panel: 2 }, + { name: 'Default layout V2', icon: 'document', panel: 4 }, { name: 'Canvas Layout Option', icon: 'canvasApp', panel: 3 }, ], }, + { + id: 7, + initialFocusedItemIndex: 0, + title: 'PNG Reports', + items: [{ name: 'Default layout V2', icon: 'document', panel: 5 }], + }, { id: 2, - title: 'No Layout Option', + title: 'Default layout', content: ( ), }, + { + id: 4, + title: 'Default layout V2', + content: ( + + ), + }, + { + id: 5, + title: 'Default layout V2', + content: ( + + ), + }, ]; return ( @@ -124,9 +202,11 @@ export const ReportingExampleApp = ({ + +

Example of a Sharing menu using components from Reporting

+
+ -

Example of a Sharing menu using components from Reporting

- Share} @@ -140,8 +220,29 @@ export const ReportingExampleApp = ({ -
+
+ + {forwardedParams ? ( + <> + +

+ Forwarded app state +

+
+ {JSON.stringify(forwardedParams)} + + ) : ( + <> + +

+ No forwarded app state found +

+
+ {'{}'} + + )} +
{logos.map((item, index) => ( { - public setup(core: CoreSetup, { developerExamples, screenshotMode }: SetupDeps): void { + public setup(core: CoreSetup, { developerExamples, screenshotMode, share }: SetupDeps): void { core.application.register({ id: PLUGIN_ID, title: PLUGIN_NAME, @@ -30,7 +30,12 @@ export class ReportingExamplePlugin implements Plugin { unknown ]; // Render the application - return renderApp(coreStart, { ...depsStart, screenshotMode }, params); + return renderApp( + coreStart, + { ...depsStart, screenshotMode, share }, + params, + params.history.location.state as MyForwardableState + ); }, }); @@ -40,6 +45,8 @@ export class ReportingExamplePlugin implements Plugin { title: 'Reporting integration', description: 'Demonstrate how to put an Export button on a page and generate reports.', }); + + share.url.locators.create(new ReportingExampleLocatorDefinition()); } public start() {} diff --git a/x-pack/examples/reporting_example/public/types.ts b/x-pack/examples/reporting_example/public/types.ts index 55a573285e24f..fb28293ab63a3 100644 --- a/x-pack/examples/reporting_example/public/types.ts +++ b/x-pack/examples/reporting_example/public/types.ts @@ -7,6 +7,7 @@ import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/public'; +import { SharePluginSetup } from 'src/plugins/share/public'; import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; import { ReportingStart } from '../../../plugins/reporting/public'; @@ -17,9 +18,12 @@ export interface PluginStart {} export interface SetupDeps { developerExamples: DeveloperExamplesSetup; + share: SharePluginSetup; screenshotMode: ScreenshotModePluginSetup; } export interface StartDeps { navigation: NavigationPublicPluginStart; reporting: ReportingStart; } + +export type MyForwardableState = Record; diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index be543b3908b68..4ba406a14bafc 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -61,10 +61,14 @@ export const CSV_REPORT_TYPE = 'CSV'; export const CSV_JOB_TYPE = 'csv_searchsource'; export const PDF_REPORT_TYPE = 'printablePdf'; +export const PDF_REPORT_TYPE_V2 = 'printablePdfV2'; export const PDF_JOB_TYPE = 'printable_pdf'; +export const PDF_JOB_TYPE_V2 = 'printable_pdf_v2'; export const PNG_REPORT_TYPE = 'PNG'; +export const PNG_REPORT_TYPE_V2 = 'pngV2'; export const PNG_JOB_TYPE = 'PNG'; +export const PNG_JOB_TYPE_V2 = 'PNGV2'; export const CSV_SEARCHSOURCE_IMMEDIATE_TYPE = 'csv_searchsource_immediate'; @@ -98,6 +102,20 @@ export const ILM_POLICY_NAME = 'kibana-reporting'; // Management UI route export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; +export const REPORTING_REDIRECT_LOCATOR_STORE_KEY = '__REPORTING_REDIRECT_LOCATOR_STORE_KEY__'; + +/** + * A way to get the client side route for the reporting redirect app. + * + * This route currently expects a job ID and a locator that to use from that job so that it can redirect to the + * correct page. + * + * TODO: Accommodate 'forceNow' value that some visualizations may rely on + */ +export const getRedirectAppPathHome = () => { + return '/app/management/insightsAndAlerting/reporting/r'; +}; + // Statuses export enum JOB_STATUSES { PENDING = 'pending', diff --git a/x-pack/plugins/reporting/common/job_utils.ts b/x-pack/plugins/reporting/common/job_utils.ts new file mode 100644 index 0000000000000..1a8699eeca025 --- /dev/null +++ b/x-pack/plugins/reporting/common/job_utils.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +// TODO: Remove this code once everyone is using the new PDF format, then we can also remove the legacy +// export type entirely +export const isJobV2Params = ({ sharingData }: { sharingData: Record }): boolean => + Array.isArray(sharingData.locatorParams); diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 7bcf69b564b3c..42e8e9c52719c 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { SerializableRecord } from '@kbn/utility-types'; + export interface PageSizeParams { pageMarginTop: number; pageMarginBottom: number; @@ -64,6 +66,11 @@ export interface ReportSource { created_by: string | false; // username or `false` if security is disabled. Used for ensuring users can only access the reports they've created. payload: { headers: string; // encrypted headers + /** + * PDF V2 reports will contain locators parameters (see {@link LocatorPublic}) that will be converted to {@link KibanaLocation}s when + * generating a report + */ + locatorParams?: LocatorParams[]; isDeprecated?: boolean; // set to true when the export type is being phased out } & BaseParams; meta: { objectType: string; layout?: string }; // for telemetry @@ -167,8 +174,21 @@ export type DownloadReportFn = (jobId: JobId) => DownloadLink; type ManagementLink = string; export type ManagementLinkFn = () => ManagementLink; +export interface LocatorParams< + P extends SerializableRecord = SerializableRecord & { forceNow?: string } +> { + id: string; + version: string; + params: P; +} + export type IlmPolicyMigrationStatus = 'policy-not-found' | 'indices-not-managed-by-policy' | 'ok'; export interface IlmPolicyStatusResponse { status: IlmPolicyMigrationStatus; } + +type Url = string; +type UrlLocatorTuple = [url: Url, locatorParams: LocatorParams]; + +export type UrlOrUrlLocatorTuple = Url | UrlLocatorTuple; diff --git a/x-pack/plugins/reporting/public/constants.ts b/x-pack/plugins/reporting/public/constants.ts new file mode 100644 index 0000000000000..c7e77fd44a780 --- /dev/null +++ b/x-pack/plugins/reporting/public/constants.ts @@ -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 const REACT_ROUTER_REDIRECT_APP_PATH = '/r'; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx b/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx index 4070f0d6d388d..d53c69ae22e7f 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/context.tsx @@ -19,7 +19,7 @@ interface ContextValue { const InternalApiClientContext = createContext(undefined); -export const InternalApiClientClientProvider: FunctionComponent<{ +export const InternalApiClientProvider: FunctionComponent<{ apiClient: ReportingAPIClient; }> = ({ apiClient, children }) => { const { diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts index b32d675a1d209..7439bf8bca900 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/index.ts @@ -9,4 +9,4 @@ export * from './reporting_api_client'; export * from './hooks'; -export { InternalApiClientClientProvider, useInternalApiClient } from './context'; +export { InternalApiClientProvider, useInternalApiClient } from './context'; diff --git a/x-pack/plugins/reporting/public/management/mount_management_section.tsx b/x-pack/plugins/reporting/public/management/mount_management_section.tsx index 0f0c06f830205..56ede79086a04 100644 --- a/x-pack/plugins/reporting/public/management/mount_management_section.tsx +++ b/x-pack/plugins/reporting/public/management/mount_management_section.tsx @@ -11,7 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Observable } from 'rxjs'; import { CoreSetup, CoreStart } from 'src/core/public'; import { ILicense } from '../../../licensing/public'; -import { ReportingAPIClient, InternalApiClientClientProvider } from '../lib/reporting_api_client'; +import { ReportingAPIClient, InternalApiClientProvider } from '../lib/reporting_api_client'; import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; import { ClientConfigType } from '../plugin'; import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; @@ -32,7 +32,7 @@ export async function mountManagementSection( - + - + , params.element diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index dd8b60801066f..bae396901dc44 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -21,7 +21,7 @@ import type { ILicense } from '../../../licensing/public'; import { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types'; import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; import { Job } from '../lib/job'; -import { InternalApiClientClientProvider, ReportingAPIClient } from '../lib/reporting_api_client'; +import { InternalApiClientProvider, ReportingAPIClient } from '../lib/reporting_api_client'; import { KibanaContextProvider } from '../shared_imports'; import { ListingProps as Props, ReportListing } from '.'; @@ -84,7 +84,7 @@ describe('ReportListing', () => { const createTestBed = registerTestBed( (props?: Partial) => ( - + { {...props} /> - + ), { memoryRouter: { wrapComponent: false } } diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 757f226532d95..2529681a6901f 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -42,6 +42,7 @@ import type { } from './shared_imports'; import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; +import { isRedirectAppPath } from './utils'; export interface ClientConfigType { poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } }; @@ -167,6 +168,15 @@ export class ReportingPublicPlugin title: this.title, order: 1, mount: async (params) => { + // The redirect app will be mounted if reporting is opened on a specific path. The redirect app expects a + // specific environment to be present so that it can navigate to a specific application. This is used by + // report generation to navigate to the correct place with full app state. + if (isRedirectAppPath(params.history.location.pathname)) { + const { mountRedirectApp } = await import('./redirect'); + return mountRedirectApp({ ...params, share, apiClient }); + } + + // Otherwise load the reporting management UI. params.setBreadcrumbs([{ text: this.breadcrumbText }]); const [[start], { mountManagementSection }] = await Promise.all([ getStartServices(), diff --git a/x-pack/plugins/reporting/public/redirect/index.ts b/x-pack/plugins/reporting/public/redirect/index.ts new file mode 100644 index 0000000000000..2cf2f0c2d11a1 --- /dev/null +++ b/x-pack/plugins/reporting/public/redirect/index.ts @@ -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 { mountRedirectApp } from './mount_redirect_app'; diff --git a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx new file mode 100644 index 0000000000000..4bf6d40acb170 --- /dev/null +++ b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx @@ -0,0 +1,33 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import React from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; + +import type { ManagementAppMountParams, SharePluginSetup } from '../shared_imports'; +import type { ReportingAPIClient } from '../lib/reporting_api_client'; + +import { RedirectApp } from './redirect_app'; + +interface MountParams extends ManagementAppMountParams { + apiClient: ReportingAPIClient; + share: SharePluginSetup; +} + +export const mountRedirectApp = ({ element, apiClient, history, share }: MountParams) => { + render( + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx new file mode 100644 index 0000000000000..60b51c0f07895 --- /dev/null +++ b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import type { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiTitle, EuiCallOut, EuiCodeBlock } from '@elastic/eui'; + +import type { ScopedHistory } from 'src/core/public'; + +import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../../common/constants'; +import { LocatorParams } from '../../common/types'; + +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import type { SharePluginSetup } from '../shared_imports'; + +interface Props { + apiClient: ReportingAPIClient; + history: ScopedHistory; + share: SharePluginSetup; +} + +const i18nTexts = { + errorTitle: i18n.translate('xpack.reporting.redirectApp.errorTitle', { + defaultMessage: 'Redirect error', + }), + redirectingTitle: i18n.translate('xpack.reporting.redirectApp.redirectingMessage', { + defaultMessage: 'Redirecting...', + }), + consoleMessagePrefix: i18n.translate( + 'xpack.reporting.redirectApp.redirectConsoleErrorPrefixLabel', + { + defaultMessage: 'Redirect page error:', + } + ), +}; + +export const RedirectApp: FunctionComponent = ({ share }) => { + const [error, setError] = useState(); + + useEffect(() => { + try { + const locatorParams = ((window as unknown) as Record)[ + REPORTING_REDIRECT_LOCATOR_STORE_KEY + ]; + + if (!locatorParams) { + throw new Error('Could not find locator for report'); + } + + share.navigate(locatorParams); + } catch (e) { + setError(e); + // eslint-disable-next-line no-console + console.error(i18nTexts.consoleMessagePrefix, e.message); + throw e; + } + }, [share]); + + return error ? ( + +

{error.message}

+ {error.stack && {error.stack}} +
+ ) : ( + +

{i18nTexts.redirectingTitle}

+
+ ); +}; diff --git a/x-pack/plugins/reporting/public/share_context_menu/index.ts b/x-pack/plugins/reporting/public/share_context_menu/index.ts index 090a80d323725..b0d6f2e6a2b52 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/index.ts +++ b/x-pack/plugins/reporting/public/share_context_menu/index.ts @@ -24,6 +24,7 @@ export interface ExportPanelShareOpts { export interface ReportingSharingData { title: string; layout: LayoutParams; + [key: string]: unknown; } export interface JobParamsProviderOptions { diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index b37e31578be6d..811d5803895db 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { ShareContext } from 'src/plugins/share/public'; import { ExportPanelShareOpts, JobParamsProviderOptions, ReportingSharingData } from '.'; +import { isJobV2Params } from '../../common/job_utils'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy'; @@ -16,11 +17,11 @@ import { ScreenCapturePanelContent } from './screen_capture_panel_content_lazy'; const getJobParams = ( apiClient: ReportingAPIClient, opts: JobParamsProviderOptions, - type: 'pdf' | 'png' + type: 'png' | 'pngV2' | 'printablePdf' | 'printablePdfV2' ) => () => { const { objectType, - sharingData: { title, layout }, + sharingData: { title, layout, locatorParams }, } = opts; const baseParams = { @@ -29,6 +30,14 @@ const getJobParams = ( title, }; + if (type === 'printablePdfV2') { + // multi locator for PDF V2 + return { ...baseParams, locatorParams: [locatorParams] }; + } else if (type === 'pngV2') { + // single locator for PNG V2 + return { ...baseParams, locatorParams }; + } + // Relative URL must have URL prefix (Spaces ID prefix), but not server basePath // Replace hashes with original RISON values. const relativeUrl = opts.shareableUrl.replace( @@ -36,7 +45,7 @@ const getJobParams = ( '' ); - if (type === 'pdf') { + if (type === 'printablePdf') { // multi URL for PDF return { ...baseParams, relativeUrls: [relativeUrl] }; } @@ -111,6 +120,16 @@ export const reportingScreenshotShareProvider = ({ defaultMessage: 'PNG Reports', }); + const jobProviderOptions: JobParamsProviderOptions = { + shareableUrl, + objectType, + sharingData, + }; + + const isV2Job = isJobV2Params(jobProviderOptions); + + const pngReportType = isV2Job ? 'pngV2' : 'png'; + const panelPng = { shareMenuItem: { name: pngPanelTitle, @@ -128,10 +147,10 @@ export const reportingScreenshotShareProvider = ({ apiClient={apiClient} toasts={toasts} uiSettings={uiSettings} - reportType="png" + reportType={pngReportType} objectId={objectId} requiresSavedState={true} - getJobParams={getJobParams(apiClient, { shareableUrl, objectType, sharingData }, 'png')} + getJobParams={getJobParams(apiClient, jobProviderOptions, pngReportType)} isDirty={isDirty} onClose={onClose} /> @@ -143,6 +162,8 @@ export const reportingScreenshotShareProvider = ({ defaultMessage: 'PDF Reports', }); + const pdfReportType = isV2Job ? 'printablePdfV2' : 'printablePdf'; + const panelPdf = { shareMenuItem: { name: pdfPanelTitle, @@ -160,11 +181,11 @@ export const reportingScreenshotShareProvider = ({ apiClient={apiClient} toasts={toasts} uiSettings={uiSettings} - reportType="printablePdf" + reportType={pdfReportType} objectId={objectId} requiresSavedState={true} layoutOption={objectType === 'dashboard' ? 'print' : undefined} - getJobParams={getJobParams(apiClient, { shareableUrl, objectType, sharingData }, 'pdf')} + getJobParams={getJobParams(apiClient, jobProviderOptions, pdfReportType)} isDirty={isDirty} onClose={onClose} /> diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx index af6cd0010de09..11169dd2d2fb7 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx @@ -21,7 +21,13 @@ import React, { Component, ReactElement } from 'react'; import { ToastsSetup, IUiSettingsClient } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants'; +import { + CSV_REPORT_TYPE, + PDF_REPORT_TYPE, + PDF_REPORT_TYPE_V2, + PNG_REPORT_TYPE, + PNG_REPORT_TYPE_V2, +} from '../../common/constants'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -200,10 +206,12 @@ class ReportingPanelContentUi extends Component { private prettyPrintReportingType = () => { switch (this.props.reportType) { case PDF_REPORT_TYPE: + case PDF_REPORT_TYPE_V2: return 'PDF'; case 'csv_searchsource': return CSV_REPORT_TYPE; case 'png': + case PNG_REPORT_TYPE_V2: return PNG_REPORT_TYPE; default: return this.props.reportType; diff --git a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx index 659eaf2678164..623e06dd74462 100644 --- a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx +++ b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx @@ -8,7 +8,7 @@ import { CoreSetup } from 'kibana/public'; import React from 'react'; import { ReportingAPIClient } from '../'; -import { PDF_REPORT_TYPE } from '../../common/constants'; +import { PDF_REPORT_TYPE, PDF_REPORT_TYPE_V2, PNG_REPORT_TYPE_V2 } from '../../common/constants'; import type { Props as PanelPropsScreenCapture } from '../share_context_menu/screen_capture_panel_content'; import { ScreenCapturePanelContent } from '../share_context_menu/screen_capture_panel_content_lazy'; @@ -16,7 +16,7 @@ interface IncludeOnCloseFn { onClose: () => void; } -type PropsPDF = Pick & IncludeOnCloseFn; +type Props = Pick & IncludeOnCloseFn; /* * As of 7.14, the only shared component is a PDF report that is suited for Canvas integration. @@ -25,7 +25,7 @@ type PropsPDF = Pick & */ export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClient) { return { - ReportingPanelPDF(props: PropsPDF) { + ReportingPanelPDF(props: Props) { return ( ); }, + ReportingPanelPDFV2(props: Props) { + return ( + + ); + }, + ReportingPanelPNGV2(props: Props) { + return ( + + ); + }, }; } diff --git a/x-pack/plugins/reporting/public/utils.ts b/x-pack/plugins/reporting/public/utils.ts new file mode 100644 index 0000000000000..f39c7ef2174ef --- /dev/null +++ b/x-pack/plugins/reporting/public/utils.ts @@ -0,0 +1,12 @@ +/* + * 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_ROUTER_REDIRECT_APP_PATH } from './constants'; + +export const isRedirectAppPath = (pathname: string) => { + return pathname.startsWith(REACT_ROUTER_REDIRECT_APP_PATH); +}; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 30b351ff90b6f..823ccc3906e49 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -10,6 +10,8 @@ import { map, truncate } from 'lodash'; import open from 'opn'; import puppeteer, { ElementHandle, EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; +import type { LocatorParams } from '../../../../common/types'; +import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../../../../common/constants'; import { getDisallowedOutgoingUrlError } from '../'; import { ReportingCore } from '../../..'; import { KBN_SCREENSHOT_MODE_HEADER } from '../../../../../../../src/plugins/screenshot_mode/server'; @@ -94,10 +96,12 @@ export class HeadlessChromiumDriver { conditionalHeaders, waitForSelector: pageLoadSelector, timeout, + locator, }: { conditionalHeaders: ConditionalHeaders; waitForSelector: string; timeout: number; + locator?: LocatorParams; }, logger: LevelLogger ): Promise { @@ -106,8 +110,27 @@ export class HeadlessChromiumDriver { // Reset intercepted request count this.interceptedCount = 0; - const enableScreenshotMode = this.core.getEnableScreenshotMode(); - await this.page.evaluateOnNewDocument(enableScreenshotMode); + /** + * Integrate with the screenshot mode plugin contract by calling this function before any other + * scripts have run on the browser page. + */ + await this.page.evaluateOnNewDocument(this.core.getEnableScreenshotMode()); + + if (locator) { + await this.page.evaluateOnNewDocument( + (key: string, value: unknown) => { + Object.defineProperty(window, key, { + configurable: false, + writable: false, + enumerable: true, + value, + }); + }, + REPORTING_REDIRECT_LOCATOR_STORE_KEY, + locator + ); + } + await this.page.setRequestInterception(true); this.registerListeners(conditionalHeaders, logger); diff --git a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts similarity index 85% rename from x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts rename to x-pack/plugins/reporting/server/export_types/common/generate_png.ts index 2af56ed9881ae..1f186010eb8bb 100644 --- a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts @@ -8,11 +8,12 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; -import { ReportingCore } from '../../../'; -import { LevelLogger } from '../../../lib'; -import { LayoutParams, PreserveLayout } from '../../../lib/layouts'; -import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; -import { ConditionalHeaders } from '../../common'; +import { ReportingCore } from '../../'; +import { UrlOrUrlLocatorTuple } from '../../../common/types'; +import { LevelLogger } from '../../lib'; +import { LayoutParams, PreserveLayout } from '../../lib/layouts'; +import { getScreenshots$, ScreenshotResults } from '../../lib/screenshots'; +import { ConditionalHeaders } from '../common'; function getBase64DecodedSize(value: string) { // @see https://en.wikipedia.org/wiki/Base64#Output_padding @@ -30,7 +31,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) { return function generatePngObservable( logger: LevelLogger, - url: string, + urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple, browserTimezone: string | undefined, conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams @@ -47,7 +48,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) { let apmBuffer: typeof apm.currentSpan; const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { logger, - urls: [url], + urlsOrUrlLocatorTuples: [urlOrUrlLocatorTuple], conditionalHeaders, layout, browserTimezone, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts similarity index 91% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts rename to x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index ebdceda0820b9..e21b7404f5ed5 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { ReportingCore } from '../../../'; +import { ReportingCore } from '../..'; import { createMockConfig, createMockConfigSchema, createMockLevelLogger, createMockReportingCore, -} from '../../../test_helpers'; -import { getConditionalHeaders } from '../../common'; +} from '../../test_helpers'; +import { getConditionalHeaders } from '.'; import { getCustomLogo } from './get_custom_logo'; let mockReportingPlugin: ReportingCore; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts similarity index 78% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts rename to x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts index d829c3483c466..983f6f41af8d9 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { ReportingCore } from '../../../'; -import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; -import { LevelLogger } from '../../../lib'; -import { ConditionalHeaders } from '../../common'; +import { ReportingCore } from '../../'; +import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; +import { LevelLogger } from '../../lib'; +import { ConditionalHeaders } from '../common'; export const getCustomLogo = async ( reporting: ReportingCore, diff --git a/x-pack/plugins/reporting/server/export_types/common/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts index 8832577281bb2..09d3236fa7b54 100644 --- a/x-pack/plugins/reporting/server/export_types/common/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -10,6 +10,9 @@ export { getConditionalHeaders } from './get_conditional_headers'; export { getFullUrls } from './get_full_urls'; export { omitBlockedHeaders } from './omit_blocked_headers'; export { validateUrls } from './validate_urls'; +export { generatePngObservableFactory } from './generate_png'; +export { getCustomLogo } from './get_custom_logo'; +export { setForceNow } from './set_force_now'; export interface TimeRangeParams { min?: Date | string | number | null; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/get_doc_options.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_doc_options.ts rename to x-pack/plugins/reporting/server/export_types/common/pdf/get_doc_options.ts diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.test.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/get_font.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.test.ts rename to x-pack/plugins/reporting/server/export_types/common/pdf/get_font.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/get_font.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts rename to x-pack/plugins/reporting/server/export_types/common/pdf/get_font.ts diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts similarity index 98% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts rename to x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts index 7813584f26e3c..58ddeb51e7a4f 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_template.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts @@ -13,7 +13,7 @@ import { StyleDictionary, TDocumentDefinitions, } from 'pdfmake/interfaces'; -import { LayoutInstance } from '../../../../lib/layouts'; +import { LayoutInstance } from '../../../lib/layouts'; import { REPORTING_TABLE_LAYOUT } from './get_doc_options'; import { getFont } from './get_font'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts similarity index 97% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts rename to x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts index 090ca995a15fc..e4c0285d2ce4f 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { PreserveLayout, PrintLayout } from '../../../../lib/layouts'; -import { createMockConfig, createMockConfigSchema } from '../../../../test_helpers'; +import { PreserveLayout, PrintLayout } from '../../../lib/layouts'; +import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; import { PdfMaker } from './'; const imageBase64 = `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts similarity index 96% rename from x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts rename to x-pack/plugins/reporting/server/export_types/common/pdf/index.ts index 4056de6cbb111..3338b83321cec 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts @@ -12,12 +12,12 @@ import _ from 'lodash'; import path from 'path'; import Printer from 'pdfmake'; import { Content, ContentImage, ContentText } from 'pdfmake/interfaces'; -import { LayoutInstance } from '../../../../lib/layouts'; +import { LayoutInstance } from '../../../lib/layouts'; import { getDocOptions, REPORTING_TABLE_LAYOUT } from './get_doc_options'; import { getFont } from './get_font'; import { getTemplate } from './get_template'; -const assetPath = path.resolve(__dirname, '..', '..', '..', 'common', 'assets'); +const assetPath = path.resolve(__dirname, '..', '..', 'common', 'assets'); const tableBorderWidth = 1; export class PdfMaker { diff --git a/x-pack/plugins/reporting/server/export_types/common/set_force_now.ts b/x-pack/plugins/reporting/server/export_types/common/set_force_now.ts new file mode 100644 index 0000000000000..ee7d613f1b8e1 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/set_force_now.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LocatorParams } from '../../../common/types'; + +/** + * Add `forceNow` to {@link LocatorParams['params']} to enable clients to set the time appropriately when + * reporting navigates to the page in Chromium. + */ +export const setForceNow = (forceNow: string) => (locator: LocatorParams): LocatorParams => { + return { + ...locator, + params: { + ...locator.params, + forceNow, + }, + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_urls.ts new file mode 100644 index 0000000000000..bcfb06784a6dc --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_urls.ts @@ -0,0 +1,34 @@ +/* + * 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 { parse as urlParse, UrlWithStringQuery } from 'url'; +import { ReportingConfig } from '../../../'; +import { getAbsoluteUrlFactory } from '../get_absolute_url'; +import { validateUrls } from '../validate_urls'; + +export function getFullUrls(config: ReportingConfig, relativeUrls: string[]) { + const [basePath, protocol, hostname, port] = [ + config.kbnConfig.get('server', 'basePath'), + config.get('kibanaServer', 'protocol'), + config.get('kibanaServer', 'hostname'), + config.get('kibanaServer', 'port'), + ] as string[]; + const getAbsoluteUrl = getAbsoluteUrlFactory({ basePath, protocol, hostname, port }); + + validateUrls(relativeUrls); + + const urls = relativeUrls.map((relativeUrl) => { + const parsedRelative: UrlWithStringQuery = urlParse(relativeUrl); + return getAbsoluteUrl({ + path: parsedRelative.pathname === null ? undefined : parsedRelative.pathname, + hash: parsedRelative.hash === null ? undefined : parsedRelative.hash, + search: parsedRelative.search === null ? undefined : parsedRelative.search, + }); + }); + + return urls; +} diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts index 34cfa66ddd5e1..61a987a8a8578 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts @@ -15,11 +15,11 @@ import { createMockConfigSchema, createMockReportingCore, } from '../../../test_helpers'; -import { generatePngObservableFactory } from '../lib/generate_png'; +import { generatePngObservableFactory } from '../../common'; import { TaskPayloadPNG } from '../types'; import { runTaskFnFactory } from './'; -jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); +jest.mock('../../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); let content: string; let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 1027e895b2cd0..c602db61bbbc8 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -16,8 +16,8 @@ import { getConditionalHeaders, getFullUrls, omitBlockedHeaders, + generatePngObservableFactory, } from '../../common'; -import { generatePngObservableFactory } from '../lib/generate_png'; import { TaskPayloadPNG } from '../types'; export const runTaskFnFactory: RunTaskFnFactory< diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/create_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/create_job.ts new file mode 100644 index 0000000000000..d04a5307f22e2 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png_v2/create_job.ts @@ -0,0 +1,29 @@ +/* + * 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 { cryptoFactory } from '../../lib'; +import { CreateJobFn, CreateJobFnFactory } from '../../types'; +import { JobParamsPNGV2, TaskPayloadPNGV2 } from './types'; + +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn +> = function createJobFactoryFn(reporting, logger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + + return async function createJob({ locatorParams, ...jobParams }, context, req) { + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); + + return { + ...jobParams, + headers: serializedEncryptedHeaders, + spaceId: reporting.getSpaceId(req, logger), + locatorParams: [locatorParams], + forceNow: new Date().toISOString(), + }; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts new file mode 100644 index 0000000000000..2fe0aff58069c --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as Rx from 'rxjs'; +import { Writable } from 'stream'; +import { ReportingCore } from '../../'; +import { CancellationToken } from '../../../common'; +import { LocatorParams } from '../../../common/types'; +import { cryptoFactory, LevelLogger } from '../../lib'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; +import { generatePngObservableFactory } from '../common'; +import { runTaskFnFactory } from './execute_job'; +import { TaskPayloadPNGV2 } from './types'; + +jest.mock('../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); + +let content: string; +let mockReporting: ReportingCore; +let stream: jest.Mocked; + +const cancellationToken = ({ + on: jest.fn(), +} as unknown) as CancellationToken; + +const mockLoggerFactory = { + get: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + })), +}; +const getMockLogger = () => new LevelLogger(mockLoggerFactory); + +const mockEncryptionKey = 'abcabcsecuresecret'; +const encryptHeaders = async (headers: Record) => { + const crypto = cryptoFactory(mockEncryptionKey); + return await crypto.encrypt(headers); +}; + +const getBasePayload = (baseObj: unknown) => baseObj as TaskPayloadPNGV2; + +beforeEach(async () => { + content = ''; + stream = ({ write: jest.fn((chunk) => (content += chunk)) } as unknown) as typeof stream; + + const mockReportingConfig = createMockConfigSchema({ + index: '.reporting-2018.10.10', + encryptionKey: mockEncryptionKey, + queue: { + indexInterval: 'daily', + timeout: Infinity, + }, + }); + + mockReporting = await createMockReportingCore(mockReportingConfig); + mockReporting.setConfig(createMockConfig(mockReportingConfig)); + + (generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn()); +}); + +afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset()); + +test(`passes browserTimezone to generatePng`, async () => { + const encryptedHeaders = await encryptHeaders({}); + const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; + generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); + + const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const browserTimezone = 'UTC'; + await runTask( + 'pngJobId', + getBasePayload({ + forceNow: 'test', + locatorParams: [{ version: 'test', id: 'test', params: {} }] as LocatorParams[], + browserTimezone, + headers: encryptedHeaders, + }), + cancellationToken, + stream + ); + + expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + LevelLogger { + "_logger": Object { + "get": [MockFunction], + }, + "_tags": Array [ + "PNGV2", + "execute", + "pngJobId", + ], + "warning": [Function], + }, + Array [ + "localhost:80undefined/app/management/insightsAndAlerting/reporting/r", + Object { + "id": "test", + "params": Object { + "forceNow": "test", + }, + "version": "test", + }, + ], + "UTC", + Object { + "conditions": Object { + "basePath": undefined, + "hostname": "localhost", + "port": 80, + "protocol": undefined, + }, + "headers": Object {}, + }, + undefined, + ], + ] + `); +}); + +test(`returns content_type of application/png`, async () => { + const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + + const generatePngObservable = await generatePngObservableFactory(mockReporting); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of('foo')); + + const { content_type: contentType } = await runTask( + 'pngJobId', + getBasePayload({ + locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], + headers: encryptedHeaders, + }), + cancellationToken, + stream + ); + expect(contentType).toBe('image/png'); +}); + +test(`returns content of generatePng getBuffer base64 encoded`, async () => { + const testContent = 'raw string from get_screenhots'; + const generatePngObservable = await generatePngObservableFactory(mockReporting); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ base64: testContent })); + + const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + await runTask( + 'pngJobId', + getBasePayload({ + locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], + headers: encryptedHeaders, + }), + cancellationToken, + stream + ); + + expect(content).toEqual(testContent); +}); diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts new file mode 100644 index 0000000000000..66e13679498aa --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import apm from 'elastic-apm-node'; +import * as Rx from 'rxjs'; +import { catchError, finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; +import { PNG_JOB_TYPE_V2, getRedirectAppPathHome } from '../../../common/constants'; +import { TaskRunResult } from '../../lib/tasks'; +import { RunTaskFn, RunTaskFnFactory } from '../../types'; +import { + decryptJobHeaders, + getConditionalHeaders, + omitBlockedHeaders, + generatePngObservableFactory, + setForceNow, +} from '../common'; +import { getFullUrls } from '../common/v2/get_full_urls'; +import { TaskPayloadPNGV2 } from './types'; + +export const runTaskFnFactory: RunTaskFnFactory< + RunTaskFn +> = function executeJobFactoryFn(reporting, parentLogger) { + const config = reporting.getConfig(); + const encryptionKey = config.get('encryptionKey'); + + return async function runTask(jobId, job, cancellationToken, stream) { + const apmTrans = apm.startTransaction('reporting execute_job pngV2', 'reporting'); + const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); + let apmGeneratePng: { end: () => void } | null | undefined; + + const generatePngObservable = await generatePngObservableFactory(reporting); + const jobLogger = parentLogger.clone([PNG_JOB_TYPE_V2, 'execute', jobId]); + const process$: Rx.Observable = Rx.of(1).pipe( + mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), + map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), + map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), + mergeMap((conditionalHeaders) => { + const relativeUrl = getRedirectAppPathHome(); + const [url] = getFullUrls(config, [relativeUrl]); + const [locatorParams] = job.locatorParams.map(setForceNow(job.forceNow)); + + apmGetAssets?.end(); + + apmGeneratePng = apmTrans?.startSpan('generate_png_pipeline', 'execute'); + return generatePngObservable( + jobLogger, + [url, locatorParams], + job.browserTimezone, + conditionalHeaders, + job.layout + ); + }), + tap(({ base64 }) => stream.write(base64)), + map(({ base64, warnings }) => ({ + content_type: 'image/png', + content: base64, + size: (base64 && base64.length) || 0, + warnings, + })), + catchError((err) => { + jobLogger.error(err); + return Rx.throwError(err); + }), + finalize(() => apmGeneratePng?.end()) + ); + + const stop$ = Rx.fromEventPattern(cancellationToken.on); + return process$.pipe(takeUntil(stop$)).toPromise(); + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/index.ts b/x-pack/plugins/reporting/server/export_types/png_v2/index.ts new file mode 100644 index 0000000000000..a2262be6b750b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png_v2/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { + LICENSE_TYPE_ENTERPRISE, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_TRIAL, + PNG_JOB_TYPE_V2 as jobType, +} from '../../../common/constants'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; +import { JobParamsPNGV2, TaskPayloadPNGV2 } from './types'; + +export const getExportType = (): ExportTypeDefinition< + CreateJobFn, + RunTaskFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'PNG', + createJobFnFactory, + runTaskFnFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_ENTERPRISE, + ], +}); diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/metadata.ts b/x-pack/plugins/reporting/server/export_types/png_v2/metadata.ts new file mode 100644 index 0000000000000..56e18b96f663a --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png_v2/metadata.ts @@ -0,0 +1,13 @@ +/* + * 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 { PNG_REPORT_TYPE_V2 } from '../../../common/constants'; + +export const metadata = { + id: PNG_REPORT_TYPE_V2, + name: 'PNG', +}; diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/types.d.ts b/x-pack/plugins/reporting/server/export_types/png_v2/types.d.ts new file mode 100644 index 0000000000000..50c857b66934b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/png_v2/types.d.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LocatorParams } from '../../../common/types'; +import type { LayoutParams } from '../../lib/layouts'; +import type { BaseParams, BasePayload } from '../../types'; + +// Job params: structure of incoming user request data +export interface JobParamsPNGV2 extends BaseParams { + layout: LayoutParams; + /** + * This value is used to re-create the same visual state as when the report was requested as well as navigate to the correct page. + */ + locatorParams: LocatorParams; +} + +// Job payload: structure of stored job data provided by create_job +export interface TaskPayloadPNGV2 extends BasePayload { + layout: LayoutParams; + forceNow: string; + /** + * Even though we only ever handle one locator for a PNG, we store it as an array for consistency with how PDFs are stored + */ + locatorParams: LocatorParams[]; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index a878c51ba02e2..1fcd448344bcf 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -16,9 +16,9 @@ import { getConditionalHeaders, getFullUrls, omitBlockedHeaders, + getCustomLogo, } from '../../common'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; -import { getCustomLogo } from '../lib/get_custom_logo'; import { TaskPayloadPDF } from '../types'; export const runTaskFnFactory: RunTaskFnFactory< diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 88b9f8dc95b94..737068eaba8b6 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -13,7 +13,7 @@ import { LevelLogger } from '../../../lib'; import { createLayout, LayoutParams } from '../../../lib/layouts'; import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; -import { PdfMaker } from './pdf'; +import { PdfMaker } from '../../common/pdf'; import { getTracker } from './tracker'; const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { @@ -50,7 +50,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { tracker.startScreenshots(); const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { logger, - urls, + urlsOrUrlLocatorTuples: urls, conditionalHeaders, layout, browserTimezone, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index 26cc402a8d509..5172bf300abc8 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -11,6 +11,7 @@ import { BaseParams, BasePayload } from '../../types'; interface BaseParamsPDF { layout: LayoutParams; forceNow?: string; + // TODO: Add comment explaining this field relativeUrls: string[]; } diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/create_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/create_job.ts new file mode 100644 index 0000000000000..b621759528e80 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/create_job.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cryptoFactory } from '../../lib'; +import { CreateJobFn, CreateJobFnFactory } from '../../types'; +import { JobParamsPDFV2, TaskPayloadPDFV2 } from './types'; + +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn +> = function createJobFactoryFn(reporting, logger) { + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + + return async function createJob(jobParams, context, req) { + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); + + return { + ...jobParams, + headers: serializedEncryptedHeaders, + spaceId: reporting.getSpaceId(req, logger), + forceNow: new Date().toISOString(), + }; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts new file mode 100644 index 0000000000000..f1d1ec82cdcdd --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts @@ -0,0 +1,126 @@ +/* + * 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. + */ + +jest.mock('./lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); + +import * as Rx from 'rxjs'; +import { Writable } from 'stream'; +import { ReportingCore } from '../../'; +import { CancellationToken } from '../../../common'; +import { LocatorParams } from '../../../common/types'; +import { cryptoFactory, LevelLogger } from '../../lib'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; +import { runTaskFnFactory } from './execute_job'; +import { generatePdfObservableFactory } from './lib/generate_pdf'; +import { TaskPayloadPDFV2 } from './types'; + +let content: string; +let mockReporting: ReportingCore; +let stream: jest.Mocked; + +const cancellationToken = ({ + on: jest.fn(), +} as unknown) as CancellationToken; + +const mockLoggerFactory = { + get: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + })), +}; +const getMockLogger = () => new LevelLogger(mockLoggerFactory); + +const mockEncryptionKey = 'testencryptionkey'; +const encryptHeaders = async (headers: Record) => { + const crypto = cryptoFactory(mockEncryptionKey); + return await crypto.encrypt(headers); +}; + +const getBasePayload = (baseObj: any) => + ({ + params: { forceNow: 'test' }, + ...baseObj, + } as TaskPayloadPDFV2); + +beforeEach(async () => { + content = ''; + stream = ({ write: jest.fn((chunk) => (content += chunk)) } as unknown) as typeof stream; + + const reportingConfig = { + 'server.basePath': '/sbp', + index: '.reports-test', + encryptionKey: mockEncryptionKey, + 'kibanaServer.hostname': 'localhost', + 'kibanaServer.port': 5601, + 'kibanaServer.protocol': 'http', + }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockReporting = await createMockReportingCore(mockSchema); + + (generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn()); +}); + +afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset()); + +test(`passes browserTimezone to generatePdf`, async () => { + const encryptedHeaders = await encryptHeaders({}); + const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; + generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + + const runTask = runTaskFnFactory(mockReporting, getMockLogger()); + const browserTimezone = 'UTC'; + await runTask( + 'pdfJobId', + getBasePayload({ + forceNow: 'test', + title: 'PDF Params Timezone Test', + locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], + browserTimezone, + headers: encryptedHeaders, + }), + cancellationToken, + stream + ); + + const tzParam = generatePdfObservable.mock.calls[0][4]; + expect(tzParam).toBe('UTC'); +}); + +test(`returns content_type of application/pdf`, async () => { + const logger = getMockLogger(); + const runTask = runTaskFnFactory(mockReporting, logger); + const encryptedHeaders = await encryptHeaders({}); + + const generatePdfObservable = await generatePdfObservableFactory(mockReporting); + (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + + const { content_type: contentType } = await runTask( + 'pdfJobId', + getBasePayload({ locatorParams: [], headers: encryptedHeaders }), + cancellationToken, + stream + ); + expect(contentType).toBe('application/pdf'); +}); + +test(`returns content of generatePdf getBuffer base64 encoded`, async () => { + const testContent = 'test content'; + const generatePdfObservable = await generatePdfObservableFactory(mockReporting); + (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); + + const runTask = runTaskFnFactory(mockReporting, getMockLogger()); + const encryptedHeaders = await encryptHeaders({}); + await runTask( + 'pdfJobId', + getBasePayload({ locatorParams: [], headers: encryptedHeaders }), + cancellationToken, + stream + ); + + expect(content).toEqual(Buffer.from(testContent).toString('base64')); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts new file mode 100644 index 0000000000000..c79f53d48e0f1 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts @@ -0,0 +1,88 @@ +/* + * 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 apm from 'elastic-apm-node'; +import * as Rx from 'rxjs'; +import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; +import { PDF_JOB_TYPE_V2 } from '../../../common/constants'; +import { TaskRunResult } from '../../lib/tasks'; +import { RunTaskFn, RunTaskFnFactory } from '../../types'; +import { + decryptJobHeaders, + getConditionalHeaders, + omitBlockedHeaders, + getCustomLogo, + setForceNow, +} from '../common'; +import { generatePdfObservableFactory } from './lib/generate_pdf'; +import { TaskPayloadPDFV2 } from './types'; + +export const runTaskFnFactory: RunTaskFnFactory< + RunTaskFn +> = function executeJobFactoryFn(reporting, parentLogger) { + const config = reporting.getConfig(); + const encryptionKey = config.get('encryptionKey'); + + return async function runTask(jobId, job, cancellationToken, stream) { + const jobLogger = parentLogger.clone([PDF_JOB_TYPE_V2, 'execute-job', jobId]); + const apmTrans = apm.startTransaction('reporting execute_job pdf_v2', 'reporting'); + const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup'); + let apmGeneratePdf: { end: () => void } | null | undefined; + + const generatePdfObservable = await generatePdfObservableFactory(reporting); + + const process$: Rx.Observable = Rx.of(1).pipe( + mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), + map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), + map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), + mergeMap((conditionalHeaders) => + getCustomLogo(reporting, conditionalHeaders, job.spaceId, jobLogger) + ), + mergeMap(({ logo, conditionalHeaders }) => { + const { browserTimezone, layout, title, locatorParams } = job; + if (apmGetAssets) apmGetAssets.end(); + + apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute'); + return generatePdfObservable( + jobLogger, + jobId, + title, + locatorParams.map(setForceNow(job.forceNow)), + browserTimezone, + conditionalHeaders, + layout, + logo + ); + }), + map(({ buffer, warnings }) => { + if (apmGeneratePdf) apmGeneratePdf.end(); + + const apmEncode = apmTrans?.startSpan('encode_pdf', 'output'); + const content = buffer?.toString('base64') || null; + apmEncode?.end(); + + stream.write(content); + + return { + content_type: 'application/pdf', + content, + size: buffer?.byteLength || 0, + warnings, + }; + }), + catchError((err) => { + jobLogger.error(err); + return Rx.throwError(err); + }) + ); + + const stop$ = Rx.fromEventPattern(cancellationToken.on); + + if (apmTrans) apmTrans.end(); + return process$.pipe(takeUntil(stop$)).toPromise(); + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/index.ts new file mode 100644 index 0000000000000..ffaf0c4567147 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { + LICENSE_TYPE_ENTERPRISE, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_TRIAL, + PDF_JOB_TYPE_V2 as jobType, +} from '../../../common/constants'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; +import { JobParamsPDFV2, TaskPayloadPDFV2 } from './types'; + +export const getExportType = (): ExportTypeDefinition< + CreateJobFn, + RunTaskFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + createJobFnFactory, + runTaskFnFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_ENTERPRISE, + ], +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts new file mode 100644 index 0000000000000..ff3ee8e52e53a --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -0,0 +1,130 @@ +/* + * 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 { groupBy, zip } from 'lodash'; +import * as Rx from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { ReportingCore } from '../../../'; +import { getRedirectAppPathHome } from '../../../../common/constants'; +import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../../common/types'; +import { LevelLogger } from '../../../lib'; +import { createLayout, LayoutParams } from '../../../lib/layouts'; +import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; +import { ConditionalHeaders } from '../../common'; +import { PdfMaker } from '../../common/pdf'; +import { getFullUrls } from '../../common/v2/get_full_urls'; +import { getTracker } from './tracker'; + +const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { + const grouped = groupBy(urlScreenshots.map((u) => u.timeRange)); + const values = Object.values(grouped); + if (values.length === 1) { + return values[0][0]; + } + + return null; +}; + +export async function generatePdfObservableFactory(reporting: ReportingCore) { + const config = reporting.getConfig(); + const captureConfig = config.get('capture'); + const { browserDriverFactory } = await reporting.getPluginStartDeps(); + + return function generatePdfObservable( + logger: LevelLogger, + jobId: string, + title: string, + locatorParams: LocatorParams[], + browserTimezone: string | undefined, + conditionalHeaders: ConditionalHeaders, + layoutParams: LayoutParams, + logo?: string + ): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { + const tracker = getTracker(); + tracker.startLayout(); + + const layout = createLayout(captureConfig, layoutParams); + logger.debug(`Layout: width=${layout.width} height=${layout.height}`); + tracker.endLayout(); + + tracker.startScreenshots(); + + /** + * For each locator we get the relative URL to the redirect app + */ + const relativeUrls = locatorParams.map(() => getRedirectAppPathHome()); + const urls = getFullUrls(reporting.getConfig(), relativeUrls); + + const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { + logger, + urlsOrUrlLocatorTuples: zip(urls, locatorParams) as UrlOrUrlLocatorTuple[], + conditionalHeaders, + layout, + browserTimezone, + }).pipe( + mergeMap(async (results: ScreenshotResults[]) => { + tracker.endScreenshots(); + + tracker.startSetup(); + const pdfOutput = new PdfMaker(layout, logo); + if (title) { + const timeRange = getTimeRange(results); + title += timeRange ? ` - ${timeRange}` : ''; + pdfOutput.setTitle(title); + } + tracker.endSetup(); + + results.forEach((r) => { + r.screenshots.forEach((screenshot) => { + logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.base64EncodedData?.length || 0}`); // prettier-ignore + tracker.startAddImage(); + tracker.endAddImage(); + pdfOutput.addImage(screenshot.base64EncodedData, { + title: screenshot.title, + description: screenshot.description, + }); + }); + }); + + let buffer: Buffer | null = null; + try { + tracker.startCompile(); + logger.debug(`Compiling PDF using "${layout.id}" layout...`); + pdfOutput.generate(); + tracker.endCompile(); + + tracker.startGetBuffer(); + logger.debug(`Generating PDF Buffer...`); + buffer = await pdfOutput.getBuffer(); + + const byteLength = buffer?.byteLength ?? 0; + logger.debug(`PDF buffer byte length: ${byteLength}`); + tracker.setByteLength(byteLength); + + tracker.endGetBuffer(); + } catch (err) { + logger.error(`Could not generate the PDF buffer!`); + logger.error(err); + } + + tracker.end(); + + return { + buffer, + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + return found; + }, [] as string[]), + }; + }) + ); + + return screenshots$; + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts new file mode 100644 index 0000000000000..4b5a0a7bdade7 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts @@ -0,0 +1,88 @@ +/* + * 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 apm from 'elastic-apm-node'; + +interface PdfTracker { + setByteLength: (byteLength: number) => void; + startLayout: () => void; + endLayout: () => void; + startScreenshots: () => void; + endScreenshots: () => void; + startSetup: () => void; + endSetup: () => void; + startAddImage: () => void; + endAddImage: () => void; + startCompile: () => void; + endCompile: () => void; + startGetBuffer: () => void; + endGetBuffer: () => void; + end: () => void; +} + +const SPANTYPE_SETUP = 'setup'; +const SPANTYPE_OUTPUT = 'output'; + +interface ApmSpan { + end: () => void; +} + +export function getTracker(): PdfTracker { + const apmTrans = apm.startTransaction('reporting generate_pdf', 'reporting'); + + let apmLayout: ApmSpan | null = null; + let apmScreenshots: ApmSpan | null = null; + let apmSetup: ApmSpan | null = null; + let apmAddImage: ApmSpan | null = null; + let apmCompilePdf: ApmSpan | null = null; + let apmGetBuffer: ApmSpan | null = null; + + return { + startLayout() { + apmLayout = apmTrans?.startSpan('create_layout', SPANTYPE_SETUP) || null; + }, + endLayout() { + if (apmLayout) apmLayout.end(); + }, + startScreenshots() { + apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', SPANTYPE_SETUP) || null; + }, + endScreenshots() { + if (apmScreenshots) apmScreenshots.end(); + }, + startSetup() { + apmSetup = apmTrans?.startSpan('setup_pdf', SPANTYPE_SETUP) || null; + }, + endSetup() { + if (apmSetup) apmSetup.end(); + }, + startAddImage() { + apmAddImage = apmTrans?.startSpan('add_pdf_image', SPANTYPE_OUTPUT) || null; + }, + endAddImage() { + if (apmAddImage) apmAddImage.end(); + }, + startCompile() { + apmCompilePdf = apmTrans?.startSpan('compile_pdf', SPANTYPE_OUTPUT) || null; + }, + endCompile() { + if (apmCompilePdf) apmCompilePdf.end(); + }, + startGetBuffer() { + apmGetBuffer = apmTrans?.startSpan('get_buffer', SPANTYPE_OUTPUT) || null; + }, + endGetBuffer() { + if (apmGetBuffer) apmGetBuffer.end(); + }, + setByteLength(byteLength: number) { + apmTrans?.setLabel('byte_length', byteLength, false); + }, + end() { + if (apmTrans) apmTrans.end(); + }, + }; +} diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/uri_encode.js b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/uri_encode.js new file mode 100644 index 0000000000000..c2170b0aaf897 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/uri_encode.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { forEach, isArray } from 'lodash'; +import { url } from '../../../../../../../src/plugins/kibana_utils/server'; + +function toKeyValue(obj) { + const parts = []; + forEach(obj, function (value, key) { + if (isArray(value)) { + forEach(value, function (arrayValue) { + const keyStr = url.encodeUriQuery(key, true); + const valStr = arrayValue === true ? '' : '=' + url.encodeUriQuery(arrayValue, true); + parts.push(keyStr + valStr); + }); + } else { + const keyStr = url.encodeUriQuery(key, true); + const valStr = value === true ? '' : '=' + url.encodeUriQuery(value, true); + parts.push(keyStr + valStr); + } + }); + return parts.length ? parts.join('&') : ''; +} + +export const uriEncode = { + stringify: toKeyValue, + string: url.encodeUriQuery, +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/metadata.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/metadata.ts new file mode 100644 index 0000000000000..f4fc93a86821b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/metadata.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const metadata = { + id: 'printablePdfV2', + name: 'PDF', +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/types.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/types.ts new file mode 100644 index 0000000000000..a629eea9f21f7 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LocatorParams } from '../../../common/types'; +import { LayoutParams } from '../../lib/layouts'; +import { BaseParams, BasePayload } from '../../types'; + +interface BaseParamsPDFV2 { + layout: LayoutParams; + + /** + * This value is used to re-create the same visual state as when the report was requested as well as navigate to the correct page. + */ + locatorParams: LocatorParams[]; +} + +// Job params: structure of incoming user request data, after being parsed from RISON +export type JobParamsPDFV2 = BaseParamsPDFV2 & BaseParams; + +// Job payload: structure of stored job data provided by create_job +export interface TaskPayloadPDFV2 extends BasePayload, BaseParamsPDFV2 { + layout: LayoutParams; + /** + * The value of forceNow is injected server-side every time a given report is generated. + */ + forceNow: string; +} diff --git a/x-pack/plugins/reporting/server/index.ts b/x-pack/plugins/reporting/server/index.ts index 999311b9ae17b..bc6529eb90782 100644 --- a/x-pack/plugins/reporting/server/index.ts +++ b/x-pack/plugins/reporting/server/index.ts @@ -21,5 +21,4 @@ export { ReportingSetupDeps as PluginSetup, ReportingStartDeps as PluginStart, } from './types'; - export { ReportingPlugin as Plugin }; diff --git a/x-pack/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/plugins/reporting/server/lib/export_types_registry.ts index 890af43297751..314d50e131565 100644 --- a/x-pack/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/plugins/reporting/server/lib/export_types_registry.ts @@ -10,7 +10,10 @@ import { getExportType as getTypeCsvDeprecated } from '../export_types/csv'; import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_searchsource_immediate'; import { getExportType as getTypeCsv } from '../export_types/csv_searchsource'; import { getExportType as getTypePng } from '../export_types/png'; +import { getExportType as getTypePngV2 } from '../export_types/png_v2'; import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf'; +import { getExportType as getTypePrintablePdfV2 } from '../export_types/printable_pdf_v2'; + import { CreateJobFn, ExportTypeDefinition } from '../types'; type GetCallbackFn = (item: ExportTypeDefinition) => boolean; @@ -88,7 +91,9 @@ export function getExportTypesRegistry(): ExportTypesRegistry { getTypeCsvDeprecated, getTypeCsvFromSavedObject, getTypePng, + getTypePngV2, getTypePrintablePdf, + getTypePrintablePdfV2, ]; getTypeFns.forEach((getType) => { registry.register(getType()); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index d5ef52d627c6b..d5920605a8be6 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -6,6 +6,7 @@ */ import { LevelLogger } from '../'; +import { UrlOrUrlLocatorTuple } from '../../../common/types'; import { ConditionalHeaders } from '../../export_types/common'; import { LayoutInstance } from '../layouts'; @@ -13,7 +14,7 @@ export { getScreenshots$ } from './observable'; export interface ScreenshotObservableOpts { logger: LevelLogger; - urls: string[]; + urlsOrUrlLocatorTuples: UrlOrUrlLocatorTuple[]; conditionalHeaders: ConditionalHeaders; layout: LayoutInstance; browserTimezone?: string; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 7458340a4a52f..e5caa2490153a 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -69,7 +69,7 @@ describe('Screenshot Observable Pipeline', () => { it('pipelines a single url into screenshot and timeRange', async () => { const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, - urls: ['/welcome/home/start/index.htm'], + urlsOrUrlLocatorTuples: ['/welcome/home/start/index.htm'], conditionalHeaders: {} as ConditionalHeaders, layout: mockLayout, browserTimezone: 'UTC', @@ -129,7 +129,10 @@ describe('Screenshot Observable Pipeline', () => { // test const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, - urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], + urlsOrUrlLocatorTuples: [ + '/welcome/home/start/index2.htm', + '/welcome/home/start/index.php3?page=./home.php', + ], conditionalHeaders: {} as ConditionalHeaders, layout: mockLayout, browserTimezone: 'UTC', @@ -228,7 +231,7 @@ describe('Screenshot Observable Pipeline', () => { const getScreenshot = async () => { return await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, - urls: [ + urlsOrUrlLocatorTuples: [ '/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php3', ], @@ -322,7 +325,7 @@ describe('Screenshot Observable Pipeline', () => { const getScreenshot = async () => { return await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, - urls: ['/welcome/home/start/index.php3?page=./home.php3'], + urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'], conditionalHeaders: {} as ConditionalHeaders, layout: mockLayout, browserTimezone: 'UTC', @@ -352,7 +355,7 @@ describe('Screenshot Observable Pipeline', () => { const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, - urls: ['/welcome/home/start/index.php3?page=./home.php3'], + urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'], conditionalHeaders: {} as ConditionalHeaders, layout: mockLayout, browserTimezone: 'UTC', diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index baaf8a4fb38ee..e833a0dfcaf60 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -34,7 +34,13 @@ interface ScreenSetupData { export function getScreenshots$( captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory, - { logger, urls, conditionalHeaders, layout, browserTimezone }: ScreenshotObservableOpts + { + logger, + urlsOrUrlLocatorTuples, + conditionalHeaders, + layout, + browserTimezone, + }: ScreenshotObservableOpts ): Rx.Observable { const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); @@ -49,8 +55,8 @@ export function getScreenshots$( apmCreatePage?.end(); exit$.subscribe({ error: () => apmTrans?.end() }); - return Rx.from(urls).pipe( - concatMap((url, index) => { + return Rx.from(urlsOrUrlLocatorTuples).pipe( + concatMap((urlOrUrlLocatorTuple, index) => { const setup$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => { // If we're moving to another page in the app, we'll want to wait for the app to tell us @@ -62,7 +68,7 @@ export function getScreenshots$( return openUrl( captureConfig, driver, - url, + urlOrUrlLocatorTuple, pageLoadSelector, conditionalHeaders, logger @@ -129,7 +135,7 @@ export function getScreenshots$( ) ); }), - take(urls.length), + take(urlsOrUrlLocatorTuples.length), toArray() ); }), diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index 377897bcc381f..588cd792bdf06 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../common/types'; import { LevelLogger, startTrace } from '../'; import { durationToNumber } from '../../../common/schema_utils'; import { HeadlessChromiumDriver } from '../../browsers'; @@ -15,19 +16,24 @@ import { CaptureConfig } from '../../types'; export const openUrl = async ( captureConfig: CaptureConfig, browser: HeadlessChromiumDriver, - url: string, - pageLoadSelector: string, + urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple, + waitForSelector: string, conditionalHeaders: ConditionalHeaders, logger: LevelLogger ): Promise => { const endTrace = startTrace('open_url', 'wait'); + let url: string; + let locator: undefined | LocatorParams; + + if (typeof urlOrUrlLocatorTuple === 'string') { + url = urlOrUrlLocatorTuple; + } else { + [url, locator] = urlOrUrlLocatorTuple; + } + try { const timeout = durationToNumber(captureConfig.timeouts.openUrl); - await browser.open( - url, - { conditionalHeaders, waitForSelector: pageLoadSelector, timeout }, - logger - ); + await browser.open(url, { conditionalHeaders, waitForSelector, timeout, locator }, logger); } catch (err) { logger.error(err); throw new Error( diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 185b47a980bfe..0090afb855ee9 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -33,11 +33,14 @@ export class ReportingPlugin } public setup(core: CoreSetup, plugins: ReportingSetupDeps) { + const { http } = core; + const { screenshotMode, features, licensing, security, spaces, taskManager } = plugins; + const reportingCore = new ReportingCore(this.logger, this.initContext); // prevent throwing errors in route handlers about async deps not being initialized // @ts-expect-error null is not assignable to object. use a boolean property to ensure reporting API is enabled. - core.http.registerRouteHandlerContext(PLUGIN_ID, () => { + http.registerRouteHandlerContext(PLUGIN_ID, () => { if (reportingCore.pluginIsStarted()) { return reportingCore.getContract(); } else { @@ -46,9 +49,6 @@ export class ReportingPlugin } }); - const { http } = core; - const { screenshotMode, features, licensing, security, spaces, taskManager } = plugins; - const router = http.createRouter(); const basePath = http.basePath; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts index 9b3260cb31da7..4dfedef93f291 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -18,9 +18,9 @@ import { import { registerDiagnoseScreenshot } from './screenshot'; import type { ReportingRequestHandlerContext } from '../../types'; -jest.mock('../../export_types/png/lib/generate_png'); +jest.mock('../../export_types/common/generate_png'); -import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png'; +import { generatePngObservableFactory } from '../../export_types/common'; type SetupServerReturn = UnwrapPromise>; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 765e0a2a4e8a2..3a89c869542b4 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -9,9 +9,8 @@ import { i18n } from '@kbn/i18n'; import { ReportingCore } from '../..'; import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server'; import { API_DIAGNOSE_URL } from '../../../common/constants'; -import { omitBlockedHeaders } from '../../export_types/common'; +import { omitBlockedHeaders, generatePngObservableFactory } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; -import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png'; import { LevelLogger as Logger } from '../../lib'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index ad0aac121106c..6086c1b9eb872 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -112,7 +112,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { const result = await jobsQuery.get(user, docId); if (!result) { - throw Boom.notFound(); + return res.notFound(); } const { jobtype: jobType } = result; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 16cd247b4d00e..0d1520f3c4d0b 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -30,11 +30,11 @@ import { ReportTaskParams } from './lib/tasks'; export interface ReportingSetupDeps { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; + screenshotMode: ScreenshotModePluginSetup; security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; taskManager: TaskManagerSetupContract; usageCollection?: UsageCollectionSetup; - screenshotMode: ScreenshotModePluginSetup; } export interface ReportingStartDeps { From c412aa1d109a8db1dc96403da53c6ab3a45565d7 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 12 Aug 2021 18:01:33 +0100 Subject: [PATCH 11/91] [ML] Fixes callout in rule editor when no filter lists created (#108369) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/components/rule_editor/scope_section.js | 6 +++--- .../ml/public/application/routing/use_resolver.test.ts | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js index 99683e90898f1..8cdfa4f6466aa 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js @@ -19,13 +19,13 @@ import { checkPermission } from '../../capabilities/check_capabilities'; import { getScopeFieldDefaults } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; import { ML_PAGES } from '../../../../common/constants/locator'; -import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana'; +import { useMlLocator, useNavigateToPath } from '../../contexts/kibana'; function NoFilterListsCallOut() { - const mlUrlGenerator = useMlUrlGenerator(); + const mlLocator = useMlLocator(); const navigateToPath = useNavigateToPath(); const redirectToFilterManagementPage = async () => { - const path = await mlUrlGenerator.createUrl({ + const path = await mlLocator.getUrl({ page: ML_PAGES.FILTER_LISTS_MANAGE, }); await navigateToPath(path, true); diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts index 1d31e252b616c..4c5c8c7b21ddd 100644 --- a/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts +++ b/x-pack/plugins/ml/public/application/routing/use_resolver.test.ts @@ -22,9 +22,6 @@ jest.mock('../contexts/kibana/use_create_url', () => { jest.mock('../contexts/kibana', () => { return { - useMlUrlGenerator: () => ({ - createUrl: jest.fn(), - }), useNavigateToPath: () => jest.fn(), useNotifications: jest.fn(), }; From 34080b20b0bc09ad1e8bf4e0dd33b0b314a9a8a9 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Thu, 12 Aug 2021 18:02:27 +0100 Subject: [PATCH 12/91] [ML] Adds initial record score to the anomalies table expanded row content (#108216) * [ML] Adds initial record score to the anomalies table expanded row content * [ML] Edits to tooltip text following review * [ML] Add check for undefined when outputting probability --- .../anomalies_table/anomaly_details.js | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js index 20e426ac37997..669a81ffd6248 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js @@ -26,6 +26,7 @@ import { EuiSpacer, EuiTabbedContent, EuiText, + EuiToolTip, } from '@elastic/eui'; import { formatHumanReadableDateTimeSeconds } from '../../../../common/util/date_utils'; @@ -47,7 +48,7 @@ function getFilterEntity(entityName, entityValue, filter) { return ; } -function getDetailsItems(anomaly, examples, filter) { +function getDetailsItems(anomaly, filter) { const source = anomaly.source; // TODO - when multivariate analyses are more common, @@ -183,11 +184,55 @@ function getDetailsItems(anomaly, examples, filter) { }); } + items.push({ + title: ( + + + {i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.recordScoreTitle', { + defaultMessage: 'Record score', + })} + + + + ), + description: Math.floor(1000 * source.record_score) / 1000, + }); + + items.push({ + title: ( + + + {i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.initialRecordScoreTitle', { + defaultMessage: 'Initial record score', + })} + + + + ), + description: Math.floor(1000 * source.initial_record_score) / 1000, + }); + items.push({ title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.probabilityTitle', { defaultMessage: 'Probability', }), - description: source.probability, + description: + source.probability !== undefined ? Number.parseFloat(source.probability).toPrecision(3) : '', }); // If there was only one cause, the actual, typical and by_field @@ -469,7 +514,7 @@ export class AnomalyDetails extends Component { } renderDetails() { - const detailItems = getDetailsItems(this.props.anomaly, this.props.examples, this.props.filter); + const detailItems = getDetailsItems(this.props.anomaly, this.props.filter); const isInterimResult = get(this.props.anomaly, 'source.is_interim', false); return ( From 320fc8b650558788a1b79081b64d3b98b51a4681 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 12 Aug 2021 10:15:06 -0700 Subject: [PATCH 13/91] test user with specific roles and permissions- for create index pattern wizard test (#107984) * test user with specific roles and permissions * added SO method logging, added test data stream to the role and modified createindexpattern function * removed unused method added in settings page * removed unused index name- logs-* * remove unused function from settings page Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/page_objects/settings_page.ts | 7 +++++- .../management/create_index_pattern_wizard.js | 24 ++++++++++++++----- x-pack/test/functional/config.js | 11 +++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index ab581beeb8b3b..5c51a8e76dcad 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -357,7 +357,12 @@ export class SettingsPageObject extends FtrService { } await this.header.waitUntilLoadingHasFinished(); - await this.clickAddNewIndexPatternButton(); + const flyOut = await this.testSubjects.exists('createAnyway'); + if (flyOut) { + await this.testSubjects.click('createAnyway'); + } else { + await this.clickAddNewIndexPatternButton(); + } await this.header.waitUntilLoadingHasFinished(); if (!isStandardIndexPattern) { await this.selectRollupIndexPatternType(); diff --git a/x-pack/test/functional/apps/management/create_index_pattern_wizard.js b/x-pack/test/functional/apps/management/create_index_pattern_wizard.js index 445e340d236e6..9061817bbce79 100644 --- a/x-pack/test/functional/apps/management/create_index_pattern_wizard.js +++ b/x-pack/test/functional/apps/management/create_index_pattern_wizard.js @@ -8,14 +8,20 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const es = getService('es'); + const security = getService('security'); const PageObjects = getPageObjects(['settings', 'common']); + const soInfo = getService('savedObjectInfo'); + const log = getService('log'); describe('"Create Index Pattern" wizard', function () { before(async function () { - // delete .kibana index and then wait for Kibana to re-create it + await soInfo.logSoTypes(log); + await security.testUser.setRoles([ + 'global_index_pattern_management_all', + 'test_logs_data_reader', + ]); await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaIndexPatterns(); }); describe('data streams', () => { @@ -43,13 +49,19 @@ export default function ({ getService, getPageObjects }) { method: 'PUT', }); + await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.createIndexPattern('test_data_stream'); + }); + }); - await es.transport.request({ - path: '/_data_stream/test_data_stream', - method: 'DELETE', - }); + after(async () => { + await kibanaServer.savedObjects.clean({ types: ['index-pattern'] }); + await es.transport.request({ + path: '/_data_stream/test_data_stream', + method: 'DELETE', }); + await security.testUser.restoreDefaults(); + await soInfo.logSoTypes(log); }); }); } diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 06e58638922d4..704ce819b5b38 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -384,6 +384,17 @@ export default async function ({ readConfigFile }) { }, }, + test_logs_data_reader: { + elasticsearch: { + indices: [ + { + names: ['test_data_stream'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }, + geoall_data_writer: { elasticsearch: { indices: [ From 1b88880a21e71968e888e4371d2ff2b041fbcfe6 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 12 Aug 2021 13:45:08 -0400 Subject: [PATCH 14/91] [RAC][Security Solution] Add to case actions in detail flyout (#108057) * add to case action in flyout * Fix most type errors * Use context menu item instead of empty button for popover items * Remove unused import * Fire action on case modal close * Update tests to use both components and remove console.log * Update mocks in unit tests * Use an onClose prop instead of closeCallbacks * Pr feedback, create shared mock and rename handler * Make app usable when timelines is not enabled * Remove unused translations --- .../all_cases/selector_modal/index.tsx | 9 +- .../events_viewer/events_viewer.test.tsx | 29 +- .../common/mock/mock_timelines_plugin.tsx | 22 ++ .../components/take_action_dropdown/index.tsx | 122 ++++++--- .../take_action_dropdown/translations.ts | 28 -- .../side_panel/event_details/footer.tsx | 3 + .../components/side_panel/index.test.tsx | 2 + .../timeline/body/actions/index.test.tsx | 31 ++- .../timeline/body/actions/index.tsx | 3 +- .../body/events/event_column_view.test.tsx | 7 +- .../components/timeline/body/index.test.tsx | 48 +++- .../cases/add_to_case_action.test.tsx | 22 ++ .../timeline/cases/add_to_case_action.tsx | 248 ++---------------- .../cases/add_to_case_action_button.tsx | 109 ++++++++ .../cases/add_to_existing_case_button.tsx | 50 ++++ .../timeline/cases/add_to_new_case_button.tsx | 51 ++++ .../actions/timeline/cases/index.tsx | 2 + .../timelines/public/hooks/use_add_to_case.ts | 230 ++++++++++++++++ .../timelines/public/methods/index.tsx | 55 +++- .../timelines/public/mock/global_state.ts | 2 + .../public/mock/mock_timeline_data.ts | 2 + x-pack/plugins/timelines/public/plugin.ts | 14 +- .../timelines/public/store/t_grid/actions.ts | 8 + .../timelines/public/store/t_grid/model.ts | 2 + .../timelines/public/store/t_grid/reducer.ts | 22 ++ x-pack/plugins/timelines/public/types.ts | 3 + 26 files changed, 810 insertions(+), 314 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx create mode 100644 x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action_button.tsx create mode 100644 x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx create mode 100644 x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_new_case_button.tsx create mode 100644 x-pack/plugins/timelines/public/hooks/use_add_to_case.ts diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx index 6e676ea58f14f..4e8334ebceec0 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx @@ -27,6 +27,7 @@ export interface AllCasesSelectorModalProps extends Owner { onRowClick: (theCase?: Case | SubCase) => void; updateCase?: (newCase: Case) => void; userCanCrud: boolean; + onClose?: () => void; } const Modal = styled(EuiModal)` @@ -43,9 +44,15 @@ const AllCasesSelectorModalComponent: React.FC = ({ onRowClick, updateCase, userCanCrud, + onClose, }) => { const [isModalOpen, setIsModalOpen] = useState(true); - const closeModal = useCallback(() => setIsModalOpen(false), []); + const closeModal = useCallback(() => { + if (onClose) { + onClose(); + } + setIsModalOpen(false); + }, [onClose]); const onClick = useCallback( (theCase?: Case | SubCase) => { closeModal(); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index e3dea824d29ef..8398618a53d68 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -30,8 +30,33 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell import { useTimelineEvents } from '../../../timelines/containers'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; - -jest.mock('../../lib/kibana'); +import { mockTimelines } from '../../mock/mock_timelines_plugin'; + +jest.mock('../../lib/kibana', () => ({ + useKibana: () => ({ + services: { + application: { + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + }, + uiSettings: { + get: jest.fn(), + }, + savedObjects: { + client: {}, + }, + timelines: { ...mockTimelines }, + }, + }), + useToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }), + useGetUserCasesPermissions: jest.fn(), + useDateFormat: jest.fn(), + useTimeZone: jest.fn(), +})); jest.mock('../../hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx new file mode 100644 index 0000000000000..efc2ce0fd6f47 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +export const mockTimelines = { + getLastUpdated: jest.fn(), + getLoadingPanel: jest.fn(), + getFieldBrowser: jest.fn().mockReturnValue(
), + getUseDraggableKeyboardWrapper: () => + jest.fn().mockReturnValue({ + onBlur: jest.fn(), + onKeyDown: jest.fn(), + }), + getAddToCasePopover: jest + .fn() + .mockReturnValue(
{'Add to case'}
), + getAddToCaseAction: jest.fn(), +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index ffaea216f3fe3..b49440c39203f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -15,13 +15,10 @@ import { TimelineEventsDetailsItem, TimelineNonEcsData } from '../../../../commo import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; import { useInvestigateInTimeline } from '../alerts_table/timeline_actions/use_investigate_in_timeline'; -/* Todo: Uncomment case action after getAddToCaseAction is split into action and modal -import { - ACTION_ADD_TO_CASE -} from '../alerts_table/translations'; +import { ACTION_ADD_TO_CASE } from '../alerts_table/translations'; import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; import { useInsertTimeline } from '../../../cases/components/use_insert_timeline'; -import { addToCaseActionItem } from './helpers'; */ +import { addToCaseActionItem } from './helpers'; import { useEventFilterAction } from '../alerts_table/timeline_actions/use_event_filter_action'; import { useHostIsolationAction } from '../host_isolation/use_host_isolation_action'; import { CHANGE_ALERT_STATUS } from './translations'; @@ -29,6 +26,7 @@ import { getFieldValue } from '../host_isolation/helpers'; import type { Ecs } from '../../../../common/ecs'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { endpointAlertCheck } from '../../../common/utils/endpoint_alert_check'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; interface ActionsData { alertStatus: Status; @@ -64,11 +62,11 @@ export const TakeActionDropdown = React.memo( onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void; timelineId: string; }) => { - /* Todo: Uncomment case action after getAddToCaseAction is split into action and modal const casePermissions = useGetUserCasesPermissions(); + const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); + const { timelines: timelinesUi } = useKibana().services; const insertTimelineHook = useInsertTimeline; - */ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const actionsData = useMemo( @@ -149,6 +147,10 @@ export const TakeActionDropdown = React.memo( onAddEventFilterClick: handleOnAddEventFilterClick, }); + const afterCaseSelection = useCallback(() => { + closePopoverHandler(); + }, [closePopoverHandler]); + const { actionItems } = useAlertsActions({ alertStatus: actionsData.alertStatus, eventId: actionsData.eventId, @@ -176,42 +178,76 @@ export const TakeActionDropdown = React.memo( [eventFilterActions, exceptionActions, isEvent, actionsData.ruleId] ); - const panels = useMemo( - () => [ - { - id: 0, - items: [ - ...alertsActionItems, - /* Todo: Uncomment case action after getAddToCaseAction is split into action and modal - ...addToCaseActionItem(timelineId),*/ - ...hostIsolationAction, - ...investigateInTimelineAction, - ], - }, - { - id: 1, - title: CHANGE_ALERT_STATUS, - content: , - }, - /* Todo: Uncomment case action after getAddToCaseAction is split into action and modal - { - id: 2, - title: ACTION_ADD_TO_CASE, - content: ( - <> - {ecsData && - timelinesUi.getAddToCaseAction({ - ecsRowData: ecsData, - useInsertTimeline: insertTimelineHook, - casePermissions, - showIcon: false, - })} - - ), - },*/ - ], - [actionItems, alertsActionItems, hostIsolationAction, investigateInTimelineAction] - ); + const panels = useMemo(() => { + if (tGridEnabled) { + return [ + { + id: 0, + items: [ + ...alertsActionItems, + ...addToCaseActionItem(timelineId), + ...hostIsolationAction, + ...investigateInTimelineAction, + ], + }, + { + id: 1, + title: CHANGE_ALERT_STATUS, + content: , + }, + { + id: 2, + title: ACTION_ADD_TO_CASE, + content: [ + <> + {ecsData && + timelinesUi.getAddToExistingCaseButton({ + ecsRowData: ecsData, + useInsertTimeline: insertTimelineHook, + casePermissions, + appId: 'securitySolution', + onClose: afterCaseSelection, + })} + , + <> + {ecsData && + timelinesUi.getAddToNewCaseButton({ + ecsRowData: ecsData, + useInsertTimeline: insertTimelineHook, + casePermissions, + appId: 'securitySolution', + onClose: afterCaseSelection, + })} + , + ], + }, + ]; + } else { + return [ + { + id: 0, + items: [...alertsActionItems, ...hostIsolationAction, ...investigateInTimelineAction], + }, + { + id: 1, + title: CHANGE_ALERT_STATUS, + content: , + }, + ]; + } + }, [ + alertsActionItems, + hostIsolationAction, + investigateInTimelineAction, + ecsData, + casePermissions, + insertTimelineHook, + timelineId, + timelinesUi, + actionItems, + afterCaseSelection, + tGridEnabled, + ]); const takeActionButton = useMemo(() => { return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/translations.ts b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/translations.ts index f8ddb98a7ed86..09177c7de4623 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/translations.ts @@ -13,31 +13,3 @@ export const CHANGE_ALERT_STATUS = i18n.translate( defaultMessage: 'Change alert status', } ); - -export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate( - 'xpack.securitySolution.endpoint.takeAction.addEndpointException', - { - defaultMessage: 'Add Endpoint exception', - } -); - -export const ACTION_ADD_EXCEPTION = i18n.translate( - 'xpack.securitySolution.endpoint.takeAction.addException', - { - defaultMessage: 'Add rule exception', - } -); - -export const ACTION_ADD_EVENT_FILTER = i18n.translate( - 'xpack.securitySolution.endpoint.takeAction.addEventFilter', - { - defaultMessage: 'Add Endpoint event filter', - } -); - -export const INVESTIGATE_IN_TIMELINE = i18n.translate( - 'xpack.securitySolution.endpoint.takeAction.investigateInTimeline', - { - defaultMessage: 'investigate in timeline', - } -); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx index cb8ed537543a0..45737f618462d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx @@ -17,12 +17,14 @@ import { useEventFilterModal } from '../../../../detections/components/alerts_ta import { getFieldValue } from '../../../../detections/components/host_isolation/helpers'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data'; +import { Ecs } from '../../../../../common/ecs'; interface EventDetailsFooterProps { detailsData: TimelineEventsDetailsItem[] | null; expandedEvent: { eventId: string; indexName: string; + ecsData?: Ecs; refetch?: () => void; }; handleOnEventClosed: () => void; @@ -103,6 +105,7 @@ export const EventDetailsFooter = React.memo( { const state: State = { ...mockGlobalState }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 66deeddaf03f2..f70ebfc31e2c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -12,6 +12,7 @@ import { TestProviders, mockTimelineModel, mockTimelineData } from '../../../../ import { Actions } from '.'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; jest.mock('../../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; @@ -20,13 +21,29 @@ jest.mock('../../../../../common/hooks/use_selector', () => ({ useShallowEqualSelector: jest.fn(), })); -jest.mock('../../../../../common/lib/kibana', () => { - const useKibana = jest.requireActual('../../../../../common/lib/kibana'); - return { - ...useKibana, - useGetUserCasesPermissions: jest.fn(), - }; -}); +jest.mock('../../../../../common/lib/kibana', () => ({ + useKibana: () => ({ + services: { + application: { + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + }, + uiSettings: { + get: jest.fn(), + }, + savedObjects: { + client: {}, + }, + timelines: { ...mockTimelines }, + }, + }), + useToasts: jest.fn().mockReturnValue({ + addError: jest.fn(), + addSuccess: jest.fn(), + addWarning: jest.fn(), + }), + useGetUserCasesPermissions: jest.fn(), +})); describe('Actions', () => { beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index ab34ea37efeac..803e688586a31 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -185,7 +185,7 @@ const ActionsComponent: React.FC = ({ TimelineId.detectionsRulesDetailsPage, TimelineId.active, ].includes(timelineId as TimelineId) && - timelinesUi.getAddToCaseAction(addToCaseActionProps)} + timelinesUi.getAddToCasePopover(addToCaseActionProps)} = ({ onRuleChange={onRuleChange} /> + {timelinesUi.getAddToCaseAction(addToCaseActionProps)} ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index 6e8e9d678c179..088686e700da8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -20,6 +20,7 @@ import { useShallowEqualSelector } from '../../../../../common/hooks/use_selecto import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { defaultControlColumn } from '../control_columns'; import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeline_control_columns'; +import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; jest.mock('../../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; @@ -27,9 +28,7 @@ jest.mock('../../../../../common/hooks/use_selector'); jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ services: { - timelines: { - getAddToCaseAction: () =>
{'Add to case'}
, - }, + timelines: { ...mockTimelines }, }, }), useToasts: jest.fn().mockReturnValue({ @@ -44,7 +43,7 @@ jest.mock( '../../../../../../../timelines/public/components/actions/timeline/cases/add_to_case_action', () => { return { - AddToCaseAction: () => { + AddToCasePopover: () => { return
{'Add to case'}
; }, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 87fb4ee762ab0..3f805a21afa6b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -14,6 +14,8 @@ import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { Direction } from '../../../../../common/search_strategy'; import { defaultHeaders, mockTimelineData, mockTimelineModel } from '../../../../common/mock'; import { TestProviders } from '../../../../common/mock/test_providers'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; import { BodyComponent, StatefulBodyProps } from '.'; import { Sort } from './sort'; @@ -23,7 +25,44 @@ import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { defaultRowRenderers } from './renderers'; -jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana/hooks'); +jest.mock('../../../../common/hooks/use_app_toasts'); + +jest.mock('../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + }, + uiSettings: { + get: jest.fn(), + }, + savedObjects: { + client: {}, + }, + timelines: { + getLastUpdated: jest.fn(), + getLoadingPanel: jest.fn(), + getFieldBrowser: jest.fn(), + getUseDraggableKeyboardWrapper: () => + jest.fn().mockReturnValue({ + onBlur: jest.fn(), + onKeyDown: jest.fn(), + }), + getAddToCasePopover: jest + .fn() + .mockReturnValue(
{'Add to case'}
), + getAddToCaseAction: jest.fn(), + }, + }, + }), + useGetUserSavedObjectPermissions: jest.fn(), + }; +}); const mockSort: Sort[] = [ { @@ -69,6 +108,13 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({ describe('Body', () => { const mount = useMountAppended(); const mockRefetch = jest.fn(); + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + const props: StatefulBodyProps = { activePage: 0, browserFields: mockBrowserFields, diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx index bdc1171049e2e..9ff90b50d0ad4 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx @@ -10,6 +10,7 @@ import { mount } from 'enzyme'; import { TestProviders, mockGetAllCasesSelectorModal } from '../../../../mock'; import { AddToCaseAction } from './add_to_case_action'; import { SECURITY_SOLUTION_OWNER } from '../../../../../../cases/common'; +import { AddToCaseActionButton } from './add_to_case_action_button'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -30,6 +31,7 @@ describe('AddToCaseAction', () => { read: true, }, appId: 'securitySolution', + onClose: () => null, }; beforeEach(() => { @@ -39,6 +41,7 @@ describe('AddToCaseAction', () => { it('it renders', () => { const wrapper = mount( + ); @@ -49,6 +52,7 @@ describe('AddToCaseAction', () => { it('it opens the context menu', () => { const wrapper = mount( + ); @@ -61,6 +65,7 @@ describe('AddToCaseAction', () => { it('it opens the create case modal', () => { const wrapper = mount( + ); @@ -73,6 +78,7 @@ describe('AddToCaseAction', () => { it('it opens the all cases modal', () => { const wrapper = mount( + ); @@ -86,6 +92,14 @@ describe('AddToCaseAction', () => { it('it set rule information as null when missing', () => { const wrapper = mount( + { it('disabled when event type is not supported', () => { const wrapper = mount( + { }; const wrapper = mount( + ); diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx index 6df6713671a55..4b7e44112b24b 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx @@ -5,25 +5,15 @@ * 2.0. */ -import { isEmpty } from 'lodash'; -import React, { memo, useState, useCallback, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; -import { - EuiPopover, - EuiButtonIcon, - EuiContextMenuPanel, - EuiText, - EuiContextMenuItem, - EuiToolTip, -} from '@elastic/eui'; - -import { Case, CaseStatuses, StatusAll } from '../../../../../../cases/common'; +import React, { memo, useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { CaseStatuses, StatusAll } from '../../../../../../cases/common'; import { Ecs } from '../../../../../common/ecs'; +import { useAddToCase } from '../../../../hooks/use_add_to_case'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { TimelinesStartServices } from '../../../../types'; -import { ActionIconItem } from '../../action_icon_item'; import { CreateCaseFlyout } from './create/flyout'; -import { createUpdateSuccessToaster } from './helpers'; +import { tGridActions } from '../../../../'; import * as i18n from './translations'; export interface AddToCaseActionProps { @@ -35,45 +25,7 @@ export interface AddToCaseActionProps { read: boolean; } | null; appId: string; -} -interface UseControlsReturn { - isControlOpen: boolean; - openControl: () => void; - closeControl: () => void; -} - -const appendSearch = (search?: string) => - isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`; - -const getCreateCaseUrl = (search?: string | null) => `/create${appendSearch(search ?? undefined)}`; - -const getCaseDetailsUrl = ({ - id, - search, - subCaseId, -}: { - id: string; - search?: string | null; - subCaseId?: string; -}) => { - if (subCaseId) { - return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent(subCaseId)}${appendSearch( - search ?? undefined - )}`; - } - return `/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`; -}; -interface PostCommentArg { - caseId: string; - data: { - type: 'alert'; - alertId: string | string[]; - index: string | string[]; - rule: { id: string | null; name: string | null }; - owner: string; - }; - updateCase?: (newCase: Case) => void; - subCaseId?: string; + onClose?: Function; } const AddToCaseActionComponent: React.FC = ({ @@ -82,166 +34,22 @@ const AddToCaseActionComponent: React.FC = ({ useInsertTimeline, casePermissions, appId, + onClose, }) => { const eventId = ecsRowData._id; const eventIndex = ecsRowData._index; const rule = ecsRowData.signal?.rule; + const dispatch = useDispatch(); + const { cases } = useKibana().services; const { - application: { navigateToApp, getUrlForApp }, - cases, - notifications: { toasts }, - } = useKibana().services; - - const useControl = (): UseControlsReturn => { - const [isControlOpen, setIsControlOpen] = useState(false); - const openControl = useCallback(() => setIsControlOpen(true), []); - const closeControl = useCallback(() => setIsControlOpen(false), []); - - return { isControlOpen, openControl, closeControl }; - }; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const openPopover = useCallback(() => setIsPopoverOpen(true), []); - const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const isEventSupported = !isEmpty(ecsRowData.signal?.rule?.id); - const userCanCrud = casePermissions?.crud ?? false; - const isDisabled = !userCanCrud || !isEventSupported; - const tooltipContext = userCanCrud - ? isEventSupported - ? i18n.ACTION_ADD_TO_CASE_TOOLTIP - : i18n.UNSUPPORTED_EVENTS_MSG - : i18n.PERMISSIONS_MSG; - - const onViewCaseClick = useCallback( - (id) => { - navigateToApp(appId, { - deepLinkId: appId === 'securitySolution' ? 'case' : 'cases', - path: getCaseDetailsUrl({ id }), - }); - }, - [navigateToApp, appId] - ); - const currentSearch = useLocation().search; - const urlSearch = useMemo(() => currentSearch, [currentSearch]); - const createCaseUrl = useMemo(() => getUrlForApp('cases') + getCreateCaseUrl(urlSearch), [ - getUrlForApp, - urlSearch, - ]); - - const { - isControlOpen: isCreateCaseFlyoutOpen, - openControl: openCaseFlyoutOpen, - closeControl: closeCaseFlyoutOpen, - } = useControl(); - - const attachAlertToCase = useCallback( - async ( - theCase: Case, - postComment?: (arg: PostCommentArg) => Promise, - updateCase?: (newCase: Case) => void - ) => { - closeCaseFlyoutOpen(); - if (postComment) { - await postComment({ - caseId: theCase.id, - data: { - type: 'alert', - alertId: eventId, - index: eventIndex ?? '', - rule: { - id: rule?.id != null ? rule.id[0] : null, - name: rule?.name != null ? rule.name[0] : null, - }, - owner: appId, - }, - updateCase, - }); - } - }, - [closeCaseFlyoutOpen, eventId, eventIndex, rule, appId] - ); - const onCaseSuccess = useCallback( - async (theCase: Case) => { - closeCaseFlyoutOpen(); - createUpdateSuccessToaster(toasts, theCase, onViewCaseClick); - }, - [closeCaseFlyoutOpen, onViewCaseClick, toasts] - ); - - const goToCreateCase = useCallback( - async (ev) => { - ev.preventDefault(); - return navigateToApp(appId, { - deepLinkId: appId === 'securitySolution' ? 'case' : 'cases', - path: getCreateCaseUrl(urlSearch), - }); - }, - [navigateToApp, urlSearch, appId] - ); - const [isAllCaseModalOpen, openAllCaseModal] = useState(false); - - const onCaseClicked = useCallback( - (theCase) => { - /** - * No cases listed on the table. - * The user pressed the add new case table's button. - * We gonna open the create case modal. - */ - if (theCase == null) { - openCaseFlyoutOpen(); - } - openAllCaseModal(false); - }, - [openCaseFlyoutOpen] - ); - const addNewCaseClick = useCallback(() => { - closePopover(); - openCaseFlyoutOpen(); - }, [openCaseFlyoutOpen, closePopover]); - - const addExistingCaseClick = useCallback(() => { - closePopover(); - openAllCaseModal(true); - }, [openAllCaseModal, closePopover]); - - const items = useMemo( - () => [ - - {i18n.ACTION_ADD_NEW_CASE} - , - - {i18n.ACTION_ADD_EXISTING_CASE} - , - ], - [addExistingCaseClick, addNewCaseClick, isDisabled] - ); - - const button = useMemo( - () => ( - - - - ), - [ariaLabel, isDisabled, openPopover, tooltipContext] - ); + onCaseClicked, + goToCreateCase, + onCaseSuccess, + attachAlertToCase, + createCaseUrl, + isAllCaseModalOpen, + isCreateCaseFlyoutOpen, + } = useAddToCase({ ecsRowData, useInsertTimeline, casePermissions, appId, onClose }); const getAllCasesSelectorModalProps = useMemo(() => { return { @@ -263,6 +71,8 @@ const AddToCaseActionComponent: React.FC = ({ updateCase: onCaseSuccess, userCanCrud: casePermissions?.crud ?? false, owner: [appId], + onClose: () => + dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false })), }; }, [ casePermissions?.crud, @@ -275,25 +85,15 @@ const AddToCaseActionComponent: React.FC = ({ rule?.id, rule?.name, appId, + dispatch, ]); + const closeCaseFlyoutOpen = useCallback(() => { + dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false })); + }, [dispatch, eventId]); + return ( <> - {userCanCrud && ( - - - - - - )} {isCreateCaseFlyoutOpen && ( = ({ + ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL, + ecsRowData, + useInsertTimeline, + casePermissions, + appId, + onClose, +}) => { + const { + addNewCaseClick, + addExistingCaseClick, + isDisabled, + userCanCrud, + isEventSupported, + openPopover, + closePopover, + isPopoverOpen, + } = useAddToCase({ ecsRowData, useInsertTimeline, casePermissions, appId, onClose }); + const tooltipContext = userCanCrud + ? isEventSupported + ? i18n.ACTION_ADD_TO_CASE_TOOLTIP + : i18n.UNSUPPORTED_EVENTS_MSG + : i18n.PERMISSIONS_MSG; + const items = useMemo( + () => [ + + {i18n.ACTION_ADD_NEW_CASE} + , + + {i18n.ACTION_ADD_EXISTING_CASE} + , + ], + [addExistingCaseClick, addNewCaseClick, isDisabled] + ); + + const button = useMemo( + () => ( + + + + ), + [ariaLabel, isDisabled, openPopover, tooltipContext] + ); + + return ( + <> + {userCanCrud && ( + + + + + + )} + + ); +}; + +export const AddToCaseActionButton = memo(AddToCaseActionButtonComponent); + +// eslint-disable-next-line import/no-default-export +export default AddToCaseActionButton; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx new file mode 100644 index 0000000000000..5f0fa5332b168 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; + +import { useAddToCase } from '../../../../hooks/use_add_to_case'; +import { AddToCaseActionProps } from './add_to_case_action'; +import * as i18n from './translations'; + +const AddToCaseActionComponent: React.FC = ({ + ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL, + ecsRowData, + useInsertTimeline, + casePermissions, + appId, + onClose, +}) => { + const { addExistingCaseClick, isDisabled, userCanCrud } = useAddToCase({ + ecsRowData, + useInsertTimeline, + casePermissions, + appId, + onClose, + }); + return ( + <> + {userCanCrud && ( + + {i18n.ACTION_ADD_EXISTING_CASE} + + )} + + ); +}; + +export const AddToExistingCaseButton = memo(AddToCaseActionComponent); + +// eslint-disable-next-line import/no-default-export +export default AddToExistingCaseButton; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_new_case_button.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_new_case_button.tsx new file mode 100644 index 0000000000000..4cf81aaf40cdb --- /dev/null +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_new_case_button.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; + +import { useAddToCase } from '../../../../hooks/use_add_to_case'; +import { AddToCaseActionProps } from './add_to_case_action'; +import * as i18n from './translations'; + +const AddToCaseActionComponent: React.FC = ({ + ariaLabel = i18n.ACTION_ADD_TO_CASE_ARIA_LABEL, + ecsRowData, + useInsertTimeline, + casePermissions, + appId, + onClose, +}) => { + const { addNewCaseClick, isDisabled, userCanCrud } = useAddToCase({ + ecsRowData, + useInsertTimeline, + casePermissions, + appId, + onClose, + }); + + return ( + <> + {userCanCrud && ( + + {i18n.ACTION_ADD_NEW_CASE} + + )} + + ); +}; + +export const AddToNewCaseButton = memo(AddToCaseActionComponent); + +// eslint-disable-next-line import/no-default-export +export default AddToNewCaseButton; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/index.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/index.tsx index 1623b9dc38d61..bb3bd63e316ed 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/index.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/index.tsx @@ -7,3 +7,5 @@ export * from './add_to_case_action'; export * from './toaster_content'; +export * from './add_to_existing_case_button'; +export * from './add_to_new_case_button'; diff --git a/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts b/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts new file mode 100644 index 0000000000000..a03c1cc8f1c9a --- /dev/null +++ b/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts @@ -0,0 +1,230 @@ +/* + * 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 { useState, useCallback, useMemo, SyntheticEvent } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { Case, SubCase } from '../../../cases/common'; +import { TimelinesStartServices } from '../types'; +import { tGridActions } from '../store/t_grid'; +import { useDeepEqualSelector } from './use_selector'; +import { createUpdateSuccessToaster } from '../components/actions/timeline/cases/helpers'; +import { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action'; + +interface UseAddToCase { + addNewCaseClick: () => void; + addExistingCaseClick: () => void; + onCaseClicked: (theCase?: Case | SubCase) => void; + goToCreateCase: ( + arg: MouseEvent | React.MouseEvent | null + ) => void | Promise; + onCaseSuccess: (theCase: Case) => Promise; + attachAlertToCase: ( + theCase: Case, + postComment?: ((arg: PostCommentArg) => Promise) | undefined, + updateCase?: ((newCase: Case) => void) | undefined + ) => Promise; + createCaseUrl: string; + isAllCaseModalOpen: boolean; + isDisabled: boolean; + userCanCrud: boolean; + isEventSupported: boolean; + openPopover: (event: SyntheticEvent) => void; + closePopover: () => void; + isPopoverOpen: boolean; + isCreateCaseFlyoutOpen: boolean; +} + +const appendSearch = (search?: string) => + isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`; + +const getCreateCaseUrl = (search?: string | null) => `/create${appendSearch(search ?? undefined)}`; + +const getCaseDetailsUrl = ({ + id, + search, + subCaseId, +}: { + id: string; + search?: string | null; + subCaseId?: string; +}) => { + if (subCaseId) { + return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent(subCaseId)}${appendSearch( + search ?? undefined + )}`; + } + return `/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`; +}; +interface PostCommentArg { + caseId: string; + data: { + type: 'alert'; + alertId: string | string[]; + index: string | string[]; + rule: { id: string | null; name: string | null }; + owner: string; + }; + updateCase?: (newCase: Case) => void; + subCaseId?: string; +} + +export const useAddToCase = ({ + ecsRowData, + useInsertTimeline, + casePermissions, + appId, + onClose, +}: AddToCaseActionProps): UseAddToCase => { + const eventId = ecsRowData._id; + const eventIndex = ecsRowData._index; + const rule = ecsRowData.signal?.rule; + const dispatch = useDispatch(); + // TODO: use correct value in standalone or integrated. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const timelineById = useDeepEqualSelector((state: any) => { + if (state.timeline) { + return state.timeline.timelineById[eventId]; + } else { + return state.timelineById[eventId]; + } + }); + const isAllCaseModalOpen = useMemo(() => { + if (timelineById) { + return timelineById.isAddToExistingCaseOpen; + } else { + return false; + } + }, [timelineById]); + const isCreateCaseFlyoutOpen = useMemo(() => { + if (timelineById) { + return timelineById.isCreateNewCaseOpen; + } else { + return false; + } + }, [timelineById]); + const { + application: { navigateToApp, getUrlForApp }, + notifications: { toasts }, + } = useKibana().services; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const openPopover = useCallback(() => setIsPopoverOpen(true), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const isEventSupported = !isEmpty(ecsRowData.signal?.rule?.id); + const userCanCrud = casePermissions?.crud ?? false; + const isDisabled = !userCanCrud || !isEventSupported; + + const onViewCaseClick = useCallback( + (id) => { + navigateToApp(appId, { + deepLinkId: appId === 'securitySolution' ? 'case' : 'cases', + path: getCaseDetailsUrl({ id }), + }); + }, + [navigateToApp, appId] + ); + const currentSearch = useLocation().search; + const urlSearch = useMemo(() => currentSearch, [currentSearch]); + const createCaseUrl = useMemo(() => getUrlForApp('cases') + getCreateCaseUrl(urlSearch), [ + getUrlForApp, + urlSearch, + ]); + + const attachAlertToCase = useCallback( + async ( + theCase: Case, + postComment?: (arg: PostCommentArg) => Promise, + updateCase?: (newCase: Case) => void + ) => { + dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false })); + if (postComment) { + await postComment({ + caseId: theCase.id, + data: { + type: 'alert', + alertId: eventId, + index: eventIndex ?? '', + rule: { + id: rule?.id != null ? rule.id[0] : null, + name: rule?.name != null ? rule.name[0] : null, + }, + owner: appId, + }, + updateCase, + }); + } + }, + [eventId, eventIndex, rule, appId, dispatch] + ); + const onCaseSuccess = useCallback( + async (theCase: Case) => { + dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false })); + createUpdateSuccessToaster(toasts, theCase, onViewCaseClick); + }, + [onViewCaseClick, toasts, dispatch, eventId] + ); + + const goToCreateCase = useCallback( + async (ev) => { + ev.preventDefault(); + return navigateToApp(appId, { + deepLinkId: appId === 'securitySolution' ? 'case' : 'cases', + path: getCreateCaseUrl(urlSearch), + }); + }, + [navigateToApp, urlSearch, appId] + ); + + const onCaseClicked = useCallback( + (theCase?: Case | SubCase) => { + /** + * No cases listed on the table. + * The user pressed the add new case table's button. + * We gonna open the create case modal. + */ + if (theCase == null) { + dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: true })); + } + dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false })); + }, + [dispatch, eventId] + ); + const addNewCaseClick = useCallback(() => { + closePopover(); + dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: true })); + if (onClose) { + onClose(); + } + }, [onClose, closePopover, dispatch, eventId]); + + const addExistingCaseClick = useCallback(() => { + closePopover(); + dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: true })); + if (onClose) { + onClose(); + } + }, [onClose, closePopover, dispatch, eventId]); + return { + addNewCaseClick, + addExistingCaseClick, + onCaseClicked, + goToCreateCase, + onCaseSuccess, + attachAlertToCase, + createCaseUrl, + isAllCaseModalOpen, + isDisabled, + userCanCrud, + isEventSupported, + openPopover, + closePopover, + isPopoverOpen, + isCreateCaseFlyoutOpen, + }; +}; diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx index fa0ad55d065a3..dca72a590ea30 100644 --- a/x-pack/plugins/timelines/public/methods/index.tsx +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -7,7 +7,9 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; import type { Store } from 'redux'; +import { Provider } from 'react-redux'; import type { Storage } from '../../../../../src/plugins/kibana_utils/public'; import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import type { TGridProps } from '../types'; @@ -64,10 +66,59 @@ export const getFieldsBrowserLazy = (props: FieldBrowserProps, { store }: { stor }; const AddToCaseLazy = lazy(() => import('../components/actions/timeline/cases/add_to_case_action')); -export const getAddToCaseLazy = (props: AddToCaseActionProps) => { +export const getAddToCaseLazy = (props: AddToCaseActionProps, store: Store) => { + return ( + }> + + + + + + + ); +}; + +const AddToCasePopover = lazy( + () => import('../components/actions/timeline/cases/add_to_case_action_button') +); +export const getAddToCasePopoverLazy = (props: AddToCaseActionProps, store: Store) => { + return ( + }> + + + + + + + ); +}; + +const AddToExistingButton = lazy( + () => import('../components/actions/timeline/cases/add_to_existing_case_button') +); +export const getAddToExistingCaseButtonLazy = (props: AddToCaseActionProps, store: Store) => { + return ( + }> + + + + + + + ); +}; + +const AddToNewCaseButton = lazy( + () => import('../components/actions/timeline/cases/add_to_new_case_button') +); +export const getAddToNewCaseButtonLazy = (props: AddToCaseActionProps, store: Store) => { return ( }> - + + + + + ); }; diff --git a/x-pack/plugins/timelines/public/mock/global_state.ts b/x-pack/plugins/timelines/public/mock/global_state.ts index 610d1b26f2351..f83110eeb3135 100644 --- a/x-pack/plugins/timelines/public/mock/global_state.ts +++ b/x-pack/plugins/timelines/public/mock/global_state.ts @@ -33,6 +33,8 @@ export const mockGlobalState: TimelineState = { 'packetbeat-*', 'winlogbeat-*', ], + isAddToExistingCaseOpen: false, + isCreateNewCaseOpen: false, isLoading: false, isSelectAllChecked: false, itemsPerPage: 5, diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts index 56631c498c755..d286f518a37ee 100644 --- a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts +++ b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts @@ -1563,6 +1563,8 @@ export const mockTgridModel: TGridModel = { selectAll: false, id: 'ef579e40-jibber-jabber', indexNames: [], + isAddToExistingCaseOpen: false, + isCreateNewCaseOpen: false, isLoading: false, isSelectAllChecked: false, kqlQuery: { diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 2ec35ef1a51f3..fb1dd360d074f 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -21,6 +21,9 @@ import { getTGridLazy, getFieldsBrowserLazy, getAddToCaseLazy, + getAddToExistingCaseButtonLazy, + getAddToNewCaseButtonLazy, + getAddToCasePopoverLazy, } from './methods'; import type { TimelinesUIStart, TGridProps, TimelinesStartPlugins } from './types'; import { tGridReducer } from './store/t_grid/reducer'; @@ -79,7 +82,16 @@ export class TimelinesPlugin implements Plugin { this.setStore(store); }, getAddToCaseAction: (props) => { - return getAddToCaseLazy(props); + return getAddToCaseLazy(props, this._store!); + }, + getAddToCasePopover: (props) => { + return getAddToCasePopoverLazy(props, this._store!); + }, + getAddToExistingCaseButton: (props) => { + return getAddToExistingCaseButtonLazy(props, this._store!); + }, + getAddToNewCaseButton: (props) => { + return getAddToNewCaseButtonLazy(props, this._store!); }, }; } diff --git a/x-pack/plugins/timelines/public/store/t_grid/actions.ts b/x-pack/plugins/timelines/public/store/t_grid/actions.ts index 64c4d8a78c7ac..3c8b4e6672bd4 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/actions.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/actions.ts @@ -105,3 +105,11 @@ export const setTGridSelectAll = actionCreator<{ id: string; selectAll: boolean export const addProviderToTimeline = actionCreator<{ id: string; dataProvider: DataProvider }>( 'ADD_PROVIDER_TO_TIMELINE' ); + +export const setOpenAddToExistingCase = actionCreator<{ id: string; isOpen: boolean }>( + 'SET_OPEN_ADD_TO_EXISTING_CASE' +); + +export const setOpenAddToNewCase = actionCreator<{ id: string; isOpen: boolean }>( + 'SET_OPEN_ADD_TO_NEW_CASE' +); diff --git a/x-pack/plugins/timelines/public/store/t_grid/model.ts b/x-pack/plugins/timelines/public/store/t_grid/model.ts index 1019b1ca4a7af..757a1ed66f2fb 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/model.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/model.ts @@ -63,6 +63,8 @@ export interface TGridModel extends TGridModelSettings { /** Uniquely identifies the timeline */ id: string; indexNames: string[]; + isAddToExistingCaseOpen: boolean; + isCreateNewCaseOpen: boolean; isLoading: boolean; /** If selectAll checkbox in header is checked **/ isSelectAllChecked: boolean; diff --git a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts index 751837691ea10..e5ee2e786b2c7 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts @@ -18,6 +18,8 @@ import { setEventsDeleted, setEventsLoading, setTGridSelectAll, + setOpenAddToExistingCase, + setOpenAddToNewCase, setSelected, toggleDetailPanel, updateColumns, @@ -215,4 +217,24 @@ export const tGridReducer = reducerWithInitialState(initialTGridState) ...state, timelineById: addProviderToTimelineHelper(id, dataProvider, state.timelineById), })) + .case(setOpenAddToExistingCase, (state, { id, isOpen }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + isAddToExistingCaseOpen: isOpen, + }, + }, + })) + .case(setOpenAddToNewCase, (state, { id, isOpen }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + isCreateNewCaseOpen: isOpen, + }, + }, + })) .build(); diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index 782481d79e0c4..4d6165a9e38a7 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -41,6 +41,9 @@ export interface TimelinesUIStart { ) => UseDraggableKeyboardWrapper; setTGridEmbeddedStore: (store: Store) => void; getAddToCaseAction: (props: AddToCaseActionProps) => ReactElement; + getAddToCasePopover: (props: AddToCaseActionProps) => ReactElement; + getAddToExistingCaseButton: (props: AddToCaseActionProps) => ReactElement; + getAddToNewCaseButton: (props: AddToCaseActionProps) => ReactElement; } export interface TimelinesStartPlugins { From db9407c47ca829d35985db295f270a645f9813ac Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 12 Aug 2021 13:49:23 -0400 Subject: [PATCH 15/91] [Fleet] Link from Fleet Agent policy to integration (#108254) --- .../package_policies_table.tsx | 81 ++++++++++++------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index 4f5c61b76f4e1..86c4238e9932a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -18,6 +18,7 @@ import { EuiText, EuiIcon, EuiToolTip, + EuiLink, } from '@elastic/eui'; import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../common'; @@ -30,6 +31,7 @@ import { usePackageInstallations, useStartServices, } from '../../../../../hooks'; +import { pkgKeyFromPackageInfo } from '../../../../../services'; interface Props { packagePolicies: PackagePolicy[]; @@ -103,20 +105,30 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', { defaultMessage: 'Name', }), - render: (value: string, { description }) => ( - <> + render: (value: string, packagePolicy: InMemoryPackagePolicy) => ( + {value} - {description ? ( + {packagePolicy.description ? (   - + ) : null} - + ), }, { @@ -131,28 +143,41 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ render(packageTitle: string, packagePolicy: InMemoryPackagePolicy) { return ( - {packagePolicy.package && ( - - - - )} - {packageTitle} - {packagePolicy.package && ( - - - - - - )} + + + + {packagePolicy.package && ( + + + + )} + {packageTitle} + {packagePolicy.package && ( + + + + + + )} + + + {packagePolicy.hasUpgrade && ( <> @@ -220,7 +245,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ ], }, ], - [agentPolicy, getHref] + [agentPolicy, getHref, hasWriteCapabilities] ); return ( From 21b080b61bc8fb07e7d2d3d802dfa580a90a19a4 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 12 Aug 2021 14:05:43 -0400 Subject: [PATCH 16/91] Clean up Developer Guide (#108259) * update plugin docs * address code review feedback --- dev_docs/getting_started/add_data.mdx | 37 +++++++++ dev_docs/getting_started/dev_welcome.mdx | 3 +- .../getting_started/hello_world_plugin.mdx | 16 ++-- dev_docs/key_concepts/anatomy_of_a_plugin.mdx | 76 +++++++++++++------ dev_docs/tutorials/sample_data.mdx | 33 -------- examples/hello_world/tsconfig.json | 5 +- 6 files changed, 104 insertions(+), 66 deletions(-) create mode 100644 dev_docs/getting_started/add_data.mdx delete mode 100644 dev_docs/tutorials/sample_data.mdx diff --git a/dev_docs/getting_started/add_data.mdx b/dev_docs/getting_started/add_data.mdx new file mode 100644 index 0000000000000..b09e3f6262e77 --- /dev/null +++ b/dev_docs/getting_started/add_data.mdx @@ -0,0 +1,37 @@ +--- +id: kibDevAddData +slug: /kibana-dev-docs/tutorial/sample-data +title: Add data +summary: Learn how to add data to Kibana +date: 2021-08-11 +tags: ['kibana', 'onboarding', 'dev', 'architecture', 'tutorials'] +--- + +Building a feature and need an easy way to test it out with some data? Below are three options. + +## 1. Add Sample Data from the UI + +Kibana ships with sample data that you can install at the click of the button. If you are building a feature and need some data to test it out with, sample data is a great option. The only limitation is that this data will not work for Security or Observability solutions (see [#62962](https://github.com/elastic/kibana/issues/62962)). + +1. Navigate to the home page. +2. Click **Add data**. +3. Click on the **Sample data** tab. +4. Select a dataset by clicking on the **Add data** button. + +![Sample Data](../assets/sample_data.png) + +## CSV Upload + +1. If you don't have any data, navigate to Stack Management > Index Patterns and click the link to the uploader. If you do have data, navigate to the **Machine Learning** application. +2. Click on the **Data Visualizer** tab. +3. Click on **Select file** in the **Import data** container. + +![CSV Upload](../assets/ml_csv_upload.png) + +## makelogs + +The makelogs script generates sample web server logs. Make sure Elasticsearch is running before running the script. + +```sh +node scripts/makelogs --auth : +``` diff --git a/dev_docs/getting_started/dev_welcome.mdx b/dev_docs/getting_started/dev_welcome.mdx index 3d645b4e54d66..5e569bd377ee0 100644 --- a/dev_docs/getting_started/dev_welcome.mdx +++ b/dev_docs/getting_started/dev_welcome.mdx @@ -14,7 +14,8 @@ Kibana ships with many out-of-the-box capabilities that can be extended and enha Recommended next reading: 1. -2. Create a simple . +2. Create a . +3. . Check out our to dig into the nitty gritty details of every public plugin API. diff --git a/dev_docs/getting_started/hello_world_plugin.mdx b/dev_docs/getting_started/hello_world_plugin.mdx index d3b30b240dedc..7c02d2807472c 100644 --- a/dev_docs/getting_started/hello_world_plugin.mdx +++ b/dev_docs/getting_started/hello_world_plugin.mdx @@ -27,7 +27,7 @@ $ mkdir hello_world $ cd hello_world ``` -2. Create the . +2. Create the . ``` $ touch kibana.json @@ -44,7 +44,7 @@ and add the following: } ``` -3. Create a `tsconfig.json` file. +3. Create a . ``` $ touch tsconfig.json @@ -56,8 +56,7 @@ And add the following to it: { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types", }, "include": [ "index.ts", @@ -67,11 +66,14 @@ And add the following to it: "server/**/*.ts", "../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" } + ] } ``` -4. Create a . +4. Create a . ``` $ mkdir public @@ -104,7 +106,7 @@ export class HelloWorldPlugin implements Plugin { } ``` -5. Create a . +5. Create a . ``` $ touch index.ts diff --git a/dev_docs/key_concepts/anatomy_of_a_plugin.mdx b/dev_docs/key_concepts/anatomy_of_a_plugin.mdx index 4ff5e403ff851..fa0aae2299bb0 100644 --- a/dev_docs/key_concepts/anatomy_of_a_plugin.mdx +++ b/dev_docs/key_concepts/anatomy_of_a_plugin.mdx @@ -1,6 +1,6 @@ --- -id: kibDevTutorialBuildAPlugin -slug: /kibana-dev-docs/tutorials/anatomy-of-a-plugin +id: kibDevAnatomyOfAPlugin +slug: /kibana-dev-docs/anatomy-of-a-plugin title: Anatomy of a plugin summary: Anatomy of a Kibana plugin. date: 2021-08-03 @@ -22,22 +22,23 @@ The basic file structure of a Kibana plugin named demo that has both client-side ``` plugins/ demo - kibana.json [1] + kibana.json + tsconfig.json public - index.ts [2] - plugin.ts [3] + index.ts + plugin.ts server - index.ts [4] - plugin.ts [5] + index.ts + plugin.ts common - index.ts [6] + index.ts ``` -### [1] kibana.json +### kibana.json `kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both: -``` +```json { "id": "examplePluginId", "version": "1.0.0", @@ -88,12 +89,38 @@ plugins/ You don't need to declare a dependency on a plugin if you only wish to access its types. -### [2] public/index.ts +### tsconfig.json + +If you are developing in TypeScript (which we recommend), you will need to add a `tsconfig.json` file. Here is an example file that you would use if adding a plugin into the `examples` directory. + +```json +{ + "extends": "../../tsconfig.json", // Extend kibana/tsconfig.json + "compilerOptions": { + "outDir": "./target/types" + }, + "include": [ + "index.ts", + "../../typings/**/*", + // The following paths are optional, based on whether you have common code, + // or are building a client-side-only or server-side-only plugin. + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts" + ], + "exclude": [], + // If you import another plugin's types, point to their `tsconfig.json` file. + "references": [{ "path": "../../src/core/tsconfig.json" }] +} +``` + +### public/index.ts `public/index.ts` is the entry point into the client-side code of this plugin. Everything exported from this file will be a part of the plugins . If the plugin only exists to export static utilities, consider using a package. Otherwise, this file must export a function named plugin, which will receive a standard set of core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. -``` +```ts import type { PluginInitializerContext } from 'kibana/server'; import { DemoPlugin } from './plugin'; @@ -122,7 +149,7 @@ Using the non-`type` variation will increase the bundle size unnecessarily and m -### [3] public/plugin.ts +### public/plugin.ts `public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry point, but all plugins at Elastic should be consistent in this way. @@ -147,11 +174,11 @@ export class DemoPlugin implements Plugin { } ``` -### [4] server/index.ts +### server/index.ts `server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: -### [5] server/plugin.ts +### server/plugin.ts `server/plugin.ts` is the server-side plugin definition. The shape of this plugin is the same as it’s client-side counter-part: @@ -178,7 +205,7 @@ export class DemoPlugin implements Plugin { Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built. -### [6] common/index.ts +### common/index.ts `common/index.ts` is the entry-point into code that can be used both server-side or client side. @@ -208,13 +235,15 @@ dependency in it’s kibana.json manifest file. ** foobar plugin.ts: ** -``` +```ts import type { Plugin } from 'kibana/server'; -export interface FoobarPluginSetup { [1] +// [1] +export interface FoobarPluginSetup { getFoo(): string; } -export interface FoobarPluginStart { [1] +// [1] +export interface FoobarPluginStart { getBar(): string; } @@ -256,7 +285,8 @@ With that specified in the plugin manifest, the appropriate interfaces are then import type { CoreSetup, CoreStart } from 'kibana/server'; import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server'; -interface DemoSetupPlugins { [1]; +// [1] +interface DemoSetupPlugins { foobar: FoobarPluginSetup; } @@ -265,13 +295,15 @@ interface DemoStartPlugins { } export class DemoPlugin { - public setup(core: CoreSetup, plugins: DemoSetupPlugins) { [2]; + // [2] + public setup(core: CoreSetup, plugins: DemoSetupPlugins) { const { foobar } = plugins; foobar.getFoo(); // 'foo' foobar.getBar(); // throws because getBar does not exist } - public start(core: CoreStart, plugins: DemoStartPlugins) { [3]; + //[3] + public start(core: CoreStart, plugins: DemoStartPlugins) { const { foobar } = plugins; foobar.getFoo(); // throws because getFoo does not exist foobar.getBar(); // 'bar' diff --git a/dev_docs/tutorials/sample_data.mdx b/dev_docs/tutorials/sample_data.mdx deleted file mode 100644 index 75afaaaea6f32..0000000000000 --- a/dev_docs/tutorials/sample_data.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -id: kibDevTutorialSampleData -slug: /kibana-dev-docs/tutorial/sample-data -title: Add sample data -summary: Learn how to add sample data to Kibana -date: 2021-04-26 -tags: ['kibana', 'onboarding', 'dev', 'architecture', 'tutorials'] ---- - -## Installation from the UI - -1. Navigate to the home page. -2. Click **Add data**. -3. Click on the **Sample data** tab. -4. Select a dataset by clicking on the **Add data** button. - -![Sample Data](../assets/sample_data.png) - -## CSV Upload - -1. Navigate to the **Machine Learning** application. -2. Click on the **Data Visualizer** tab. -3. Click on **Select file** in the **Import data** container. - -![CSV Upload](../assets/ml_csv_upload.png) - -## makelogs - -The makelogs script generates sample web server logs. Make sure Elasticsearch is running before running the script. - -```sh -node scripts/makelogs --auth : -``` \ No newline at end of file diff --git a/examples/hello_world/tsconfig.json b/examples/hello_world/tsconfig.json index 06d3953e4a6bf..b494fba903415 100644 --- a/examples/hello_world/tsconfig.json +++ b/examples/hello_world/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -15,6 +14,6 @@ "exclude": [], "references": [ { "path": "../../src/core/tsconfig.json" }, - { "path": "../developer_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" } ] } From 17abc9ca15051878c69bf39ba447950c01100078 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 12 Aug 2021 11:10:56 -0700 Subject: [PATCH 17/91] [ML] Update intro text for ML in Stack Management (#108280) --- docs/user/management.asciidoc | 2 +- .../jobs_list/components/jobs_list_page/jobs_list_page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 2f9f1fe371dc3..4e5f70db9aef6 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -83,7 +83,7 @@ connectors>> for triggering actions. A report can contain a dashboard, visualization, saved search, or Canvas workpad. | Machine Learning Jobs -| View your <> and +| View, export, and import your <> and <> jobs. Open the Single Metric Viewer or Anomaly Explorer to see your {anomaly-detect} results. diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index e1ed8ee75767e..064c6a038994d 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -241,7 +241,7 @@ export const JobsListPage: FC<{ description={ } rightSideItems={[docsLink]} From 51d4b36c4e9de8d64a469f2bc6e7a8420da07b01 Mon Sep 17 00:00:00 2001 From: Marius Dragomir Date: Thu, 12 Aug 2021 20:11:14 +0200 Subject: [PATCH 18/91] update reporting URLs (#108358) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/reporting/reporting_watcher.js | 2 +- .../apps/reporting/reporting_watcher_png.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher.js b/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher.js index fb881162f51e8..eca1ca4c41cd2 100644 --- a/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher.js +++ b/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher.js @@ -40,7 +40,7 @@ export default function ({ getService, getPageObjects }) { KIBANAIP + ':' + servers.kibana.port + - '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FChicago,layout:(dimensions:(height:2024,width:3006.400146484375),id:preserve_layout),objectType:dashboard,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fdashboard%2F722b74f0-b882-11e8-a6d9-e546fe2bba5f%3F_g%3D(refreshInterval:(pause:!!f,value:900000),time:(from:now-7d,to:now))%26_a%3D(description:!%27Analyze%2Bmock%2BeCommerce%2Borders%2Band%2Brevenue!%27,filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((embeddableConfig:(vis:(colors:(!%27Men!!!%27s%2BAccessories!%27:%252382B5D8,!%27Men!!!%27s%2BClothing!%27:%2523F9BA8F,!%27Men!!!%27s%2BShoes!%27:%2523F29191,!%27Women!!!%27s%2BAccessories!%27:%2523F4D598,!%27Women!!!%27s%2BClothing!%27:%252370DBED,!%27Women!!!%27s%2BShoes!%27:%2523B7DBAB))),gridData:(h:10,i:!%271!%27,w:36,x:12,y:18),id:!%2737cc8650-b882-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%271!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(FEMALE:%25236ED0E0,MALE:%2523447EBC),legendOpen:!!f)),gridData:(h:11,i:!%272!%27,w:12,x:12,y:7),id:ed8436b0-b88b-11e8-a6d9-e546fe2bba5f,panelIndex:!%272!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:7,i:!%273!%27,w:18,x:0,y:0),id:!%2709ffee60-b88c-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%273!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:7,i:!%274!%27,w:30,x:18,y:0),id:!%271c389590-b88d-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%274!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:11,i:!%275!%27,w:48,x:0,y:28),id:!%2745e07720-b890-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%275!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:10,i:!%276!%27,w:12,x:0,y:18),id:!%2710f1a240-b891-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%276!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:11,i:!%277!%27,w:12,x:0,y:7),id:b80e6540-b891-11e8-a6d9-e546fe2bba5f,panelIndex:!%277!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(!%270%2B-%2B50!%27:%2523E24D42,!%2750%2B-%2B75!%27:%2523EAB839,!%2775%2B-%2B100!%27:%25237EB26D),defaultColors:(!%270%2B-%2B50!%27:!%27rgb(165,0,38)!%27,!%2750%2B-%2B75!%27:!%27rgb(255,255,190)!%27,!%2775%2B-%2B100!%27:!%27rgb(0,104,55)!%27),legendOpen:!!f)),gridData:(h:11,i:!%278!%27,w:12,x:24,y:7),id:!%274b3ec120-b892-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%278!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(!%270%2B-%2B2!%27:%2523E24D42,!%272%2B-%2B3!%27:%2523F2C96D,!%273%2B-%2B4!%27:%25239AC48A),defaultColors:(!%270%2B-%2B2!%27:!%27rgb(165,0,38)!%27,!%272%2B-%2B3!%27:!%27rgb(255,255,190)!%27,!%273%2B-%2B4!%27:!%27rgb(0,104,55)!%27),legendOpen:!!f)),gridData:(h:11,i:!%279!%27,w:12,x:36,y:7),id:!%279ca7aa90-b892-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%279!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:18,i:!%2710!%27,w:48,x:0,y:54),id:!%273ba638e0-b894-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%2710!%27,type:search,version:!%277.0.0-alpha1!%27),(embeddableConfig:(isLayerTOCOpen:!!f,mapCenter:(lat:45.88578,lon:-15.07605,zoom:2.11),openTOCDetails:!!()),gridData:(h:15,i:!%2711!%27,w:24,x:0,y:39),id:!%272c9c1f60-1909-11e9-919b-ffe5949a18d2!%27,panelIndex:!%2711!%27,type:map,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:15,i:!%2712!%27,w:24,x:24,y:39),id:b72dd430-bb4d-11e8-9c84-77068524bcab,panelIndex:!%2712!%27,type:visualization,version:!%277.0.0-alpha1!%27)),query:(language:kuery,query:!%27!%27),timeRestore:!!t,title:!%27%255BeCommerce%255D%2BRevenue%2BDashboard!%27,viewMode:view)%27),title:%27%5BeCommerce%5D%20Revenue%20Dashboard%27)'; + '/api/reporting/generate/printablePdf?jobParams=%28browserTimezone%3AEurope%2FParis%2Clayout%3A%28dimensions%3A%28height%3A2052%2Cwidth%3A2542.666748046875%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Adashboard%2CrelativeUrls%3A%21%28%27%2Fapp%2Fdashboards%23%2Fview%2F722b74f0-b882-11e8-a6d9-e546fe2bba5f%3F_g%3D%28filters%3A%21%21%28%29%29%26_a%3D%28description%3A%21%27Analyze%2520mock%2520eCommerce%2520orders%2520and%2520revenue%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cpanels%3A%21%21%28%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A10%2Ci%3A%21%275%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A22%29%2Cid%3A%21%2745e07720-b890-11e8-a6d9-e546fe2bba5f%21%27%2CpanelIndex%3A%21%275%21%27%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3A%21%277%21%27%2Cw%3A12%2Cx%3A36%2Cy%3A15%29%2Cid%3Ab80e6540-b891-11e8-a6d9-e546fe2bba5f%2CpanelIndex%3A%21%277%21%27%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A18%2Ci%3A%21%2710%21%27%2Cw%3A48%2Cx%3A0%2Cy%3A55%29%2Cid%3A%21%273ba638e0-b894-11e8-a6d9-e546fe2bba5f%21%27%2CpanelIndex%3A%21%2710%21%27%2Ctype%3Asearch%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChiddenLayers%3A%21%21%28%29%2CisLayerTOCOpen%3A%21%21f%2CmapBuffer%3A%28maxLat%3A66.51326%2CmaxLon%3A90%2CminLat%3A0%2CminLon%3A-135%29%2CmapCenter%3A%28lat%3A45.88578%2Clon%3A-15.07605%2Czoom%3A2.11%29%2CopenTOCDetails%3A%21%21%28%29%29%2CgridData%3A%28h%3A14%2Ci%3A%21%2711%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A32%29%2Cid%3A%21%272c9c1f60-1909-11e9-919b-ffe5949a18d2%21%27%2CpanelIndex%3A%21%2711%21%27%2Ctype%3Amap%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aa71cf076-6895-491c-8878-63592e429ed5%2Cw%3A18%2Cx%3A0%2Cy%3A0%29%2Cid%3Ac00d1f90-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aa71cf076-6895-491c-8878-63592e429ed5%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aadc0a2f4-481c-45eb-b422-0ea59a3e5163%2Cw%3A30%2Cx%3A18%2Cy%3A0%29%2Cid%3Ac3378480-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aadc0a2f4-481c-45eb-b422-0ea59a3e5163%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%29%2CgridData%3A%28h%3A8%2Ci%3A%21%277077b79f-2a99-4fcb-bbd4-456982843278%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A7%29%2Cid%3Ac762b7a0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%277077b79f-2a99-4fcb-bbd4-456982843278%21%27%2Ctitle%3A%21%27%2525%2520of%2520target%2520revenue%2520%28%2410k%29%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A8%2Ci%3A%21%2719a3c101-ad2e-4421-a71b-a4734ec1f03e%21%27%2Cw%3A12%2Cx%3A24%2Cy%3A7%29%2Cid%3Ace02e260-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%2719a3c101-ad2e-4421-a71b-a4734ec1f03e%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A8%2Ci%3A%21%27491469e7-7d24-4216-aeb3-bca00e5c8c1b%21%27%2Cw%3A12%2Cx%3A36%2Cy%3A7%29%2Cid%3Ad5f90030-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%27491469e7-7d24-4216-aeb3-bca00e5c8c1b%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aa1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef%2Cw%3A24%2Cx%3A0%2Cy%3A15%29%2Cid%3Adde978b0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aa1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Ada51079b-952f-43dc-96e6-6f9415a3708b%2Cw%3A12%2Cx%3A24%2Cy%3A15%29%2Cid%3Ae3902840-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Ada51079b-952f-43dc-96e6-6f9415a3708b%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A10%2Ci%3A%21%2764fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b%21%27%2Cw%3A24%2Cx%3A24%2Cy%3A22%29%2Cid%3Aeddf7850-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%2764fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A14%2Ci%3Abd330ede-2eef-4e2a-8100-22a21abf5038%2Cw%3A24%2Cx%3A24%2Cy%3A32%29%2Cid%3Aff6a21b0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Abd330ede-2eef-4e2a-8100-22a21abf5038%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%29%2CgridData%3A%28h%3A9%2Ci%3Ab897d4be-cf83-46fb-a111-c7fbec9ef403%2Cw%3A24%2Cx%3A0%2Cy%3A46%29%2Cid%3A%21%2703071e90-f5eb-11eb-a78e-83aac3c38a60%21%27%2CpanelIndex%3Ab897d4be-cf83-46fb-a111-c7fbec9ef403%2Ctitle%3A%21%27Top%2520products%2520this%2520week%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%2CtimeRange%3A%28from%3Anow-2w%2Cto%3Anow-1w%29%29%2CgridData%3A%28h%3A9%2Ci%3Ae0f68f93-30f2-4da7-889a-6cd128a68d3f%2Cw%3A24%2Cx%3A24%2Cy%3A46%29%2Cid%3A%21%2706379e00-f5eb-11eb-a78e-83aac3c38a60%21%27%2CpanelIndex%3Ae0f68f93-30f2-4da7-889a-6cd128a68d3f%2Ctitle%3A%21%27Top%2520products%2520last%2520week%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2Ctags%3A%21%21%28%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27%255BeCommerce%255D%2520Revenue%2520Dashboard%21%27%2CviewMode%3Aview%29%27%29%2Ctitle%3A%27%5BeCommerce%5D%20Revenue%20Dashboard%27%2Cversion%3A%278.0.0%27%29'; const body = { trigger: { diff --git a/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher_png.js b/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher_png.js index db913f563ebb0..85c00cba91fa1 100644 --- a/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher_png.js +++ b/x-pack/test/stack_functional_integration/apps/reporting/reporting_watcher_png.js @@ -33,7 +33,7 @@ export default ({ getService, getPageObjects }) => { KIBANAIP + ':' + servers.kibana.port + - '/api/reporting/generate/png?jobParams=(browserTimezone:America%2FChicago,layout:(dimensions:(height:2024,width:3006.400146484375),id:png),objectType:dashboard,relativeUrl:%27%2Fapp%2Fkibana%23%2Fdashboard%2F722b74f0-b882-11e8-a6d9-e546fe2bba5f%3F_g%3D(refreshInterval:(pause:!!f,value:900000),time:(from:now-7d,to:now))%26_a%3D(description:!%27Analyze%2Bmock%2BeCommerce%2Borders%2Band%2Brevenue!%27,filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((embeddableConfig:(vis:(colors:(!%27Men!!!%27s%2BAccessories!%27:%252382B5D8,!%27Men!!!%27s%2BClothing!%27:%2523F9BA8F,!%27Men!!!%27s%2BShoes!%27:%2523F29191,!%27Women!!!%27s%2BAccessories!%27:%2523F4D598,!%27Women!!!%27s%2BClothing!%27:%252370DBED,!%27Women!!!%27s%2BShoes!%27:%2523B7DBAB))),gridData:(h:10,i:!%271!%27,w:36,x:12,y:18),id:!%2737cc8650-b882-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%271!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(FEMALE:%25236ED0E0,MALE:%2523447EBC),legendOpen:!!f)),gridData:(h:11,i:!%272!%27,w:12,x:12,y:7),id:ed8436b0-b88b-11e8-a6d9-e546fe2bba5f,panelIndex:!%272!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:7,i:!%273!%27,w:18,x:0,y:0),id:!%2709ffee60-b88c-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%273!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:7,i:!%274!%27,w:30,x:18,y:0),id:!%271c389590-b88d-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%274!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:11,i:!%275!%27,w:48,x:0,y:28),id:!%2745e07720-b890-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%275!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:10,i:!%276!%27,w:12,x:0,y:18),id:!%2710f1a240-b891-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%276!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:11,i:!%277!%27,w:12,x:0,y:7),id:b80e6540-b891-11e8-a6d9-e546fe2bba5f,panelIndex:!%277!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(!%270%2B-%2B50!%27:%2523E24D42,!%2750%2B-%2B75!%27:%2523EAB839,!%2775%2B-%2B100!%27:%25237EB26D),defaultColors:(!%270%2B-%2B50!%27:!%27rgb(165,0,38)!%27,!%2750%2B-%2B75!%27:!%27rgb(255,255,190)!%27,!%2775%2B-%2B100!%27:!%27rgb(0,104,55)!%27),legendOpen:!!f)),gridData:(h:11,i:!%278!%27,w:12,x:24,y:7),id:!%274b3ec120-b892-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%278!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(!%270%2B-%2B2!%27:%2523E24D42,!%272%2B-%2B3!%27:%2523F2C96D,!%273%2B-%2B4!%27:%25239AC48A),defaultColors:(!%270%2B-%2B2!%27:!%27rgb(165,0,38)!%27,!%272%2B-%2B3!%27:!%27rgb(255,255,190)!%27,!%273%2B-%2B4!%27:!%27rgb(0,104,55)!%27),legendOpen:!!f)),gridData:(h:11,i:!%279!%27,w:12,x:36,y:7),id:!%279ca7aa90-b892-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%279!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:18,i:!%2710!%27,w:48,x:0,y:54),id:!%273ba638e0-b894-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%2710!%27,type:search,version:!%277.0.0-alpha1!%27),(embeddableConfig:(isLayerTOCOpen:!!f,mapCenter:(lat:45.88578,lon:-15.07605,zoom:2.11),openTOCDetails:!!()),gridData:(h:15,i:!%2711!%27,w:24,x:0,y:39),id:!%272c9c1f60-1909-11e9-919b-ffe5949a18d2!%27,panelIndex:!%2711!%27,type:map,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:15,i:!%2712!%27,w:24,x:24,y:39),id:b72dd430-bb4d-11e8-9c84-77068524bcab,panelIndex:!%2712!%27,type:visualization,version:!%277.0.0-alpha1!%27)),query:(language:kuery,query:!%27!%27),timeRestore:!!t,title:!%27%255BeCommerce%255D%2BRevenue%2BDashboard!%27,viewMode:view)%27,title:%27%5BeCommerce%5D%20Revenue%20Dashboard%27)'; + '/api/reporting/generate/png?jobParams=%28browserTimezone%3AEurope%2FParis%2Clayout%3A%28dimensions%3A%28height%3A2052%2Cwidth%3A2542.666748046875%29%2Cid%3Apreserve_layout%2Cselectors%3A%28itemsCountAttribute%3Adata-shared-items-count%2CrenderComplete%3A%5Bdata-shared-item%5D%2Cscreenshot%3A%5Bdata-shared-items-container%5D%2CtimefilterDurationAttribute%3Adata-shared-timefilter-duration%29%29%2CobjectType%3Adashboard%2CrelativeUrl%3A%27%2Fapp%2Fdashboards%23%2Fview%2F722b74f0-b882-11e8-a6d9-e546fe2bba5f%3F_g%3D%28filters%3A%21%21%28%29%29%26_a%3D%28description%3A%21%27Analyze%2520mock%2520eCommerce%2520orders%2520and%2520revenue%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cpanels%3A%21%21%28%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A10%2Ci%3A%21%275%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A22%29%2Cid%3A%21%2745e07720-b890-11e8-a6d9-e546fe2bba5f%21%27%2CpanelIndex%3A%21%275%21%27%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3A%21%277%21%27%2Cw%3A12%2Cx%3A36%2Cy%3A15%29%2Cid%3Ab80e6540-b891-11e8-a6d9-e546fe2bba5f%2CpanelIndex%3A%21%277%21%27%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A18%2Ci%3A%21%2710%21%27%2Cw%3A48%2Cx%3A0%2Cy%3A55%29%2Cid%3A%21%273ba638e0-b894-11e8-a6d9-e546fe2bba5f%21%27%2CpanelIndex%3A%21%2710%21%27%2Ctype%3Asearch%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChiddenLayers%3A%21%21%28%29%2CisLayerTOCOpen%3A%21%21f%2CmapBuffer%3A%28maxLat%3A66.51326%2CmaxLon%3A90%2CminLat%3A0%2CminLon%3A-135%29%2CmapCenter%3A%28lat%3A45.88578%2Clon%3A-15.07605%2Czoom%3A2.11%29%2CopenTOCDetails%3A%21%21%28%29%29%2CgridData%3A%28h%3A14%2Ci%3A%21%2711%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A32%29%2Cid%3A%21%272c9c1f60-1909-11e9-919b-ffe5949a18d2%21%27%2CpanelIndex%3A%21%2711%21%27%2Ctype%3Amap%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aa71cf076-6895-491c-8878-63592e429ed5%2Cw%3A18%2Cx%3A0%2Cy%3A0%29%2Cid%3Ac00d1f90-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aa71cf076-6895-491c-8878-63592e429ed5%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aadc0a2f4-481c-45eb-b422-0ea59a3e5163%2Cw%3A30%2Cx%3A18%2Cy%3A0%29%2Cid%3Ac3378480-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aadc0a2f4-481c-45eb-b422-0ea59a3e5163%2Ctype%3Avisualization%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%29%2CgridData%3A%28h%3A8%2Ci%3A%21%277077b79f-2a99-4fcb-bbd4-456982843278%21%27%2Cw%3A24%2Cx%3A0%2Cy%3A7%29%2Cid%3Ac762b7a0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%277077b79f-2a99-4fcb-bbd4-456982843278%21%27%2Ctitle%3A%21%27%2525%2520of%2520target%2520revenue%2520%28%2410k%29%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A8%2Ci%3A%21%2719a3c101-ad2e-4421-a71b-a4734ec1f03e%21%27%2Cw%3A12%2Cx%3A24%2Cy%3A7%29%2Cid%3Ace02e260-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%2719a3c101-ad2e-4421-a71b-a4734ec1f03e%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A8%2Ci%3A%21%27491469e7-7d24-4216-aeb3-bca00e5c8c1b%21%27%2Cw%3A12%2Cx%3A36%2Cy%3A7%29%2Cid%3Ad5f90030-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%27491469e7-7d24-4216-aeb3-bca00e5c8c1b%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Aa1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef%2Cw%3A24%2Cx%3A0%2Cy%3A15%29%2Cid%3Adde978b0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Aa1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A7%2Ci%3Ada51079b-952f-43dc-96e6-6f9415a3708b%2Cw%3A12%2Cx%3A24%2Cy%3A15%29%2Cid%3Ae3902840-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Ada51079b-952f-43dc-96e6-6f9415a3708b%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A10%2Ci%3A%21%2764fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b%21%27%2Cw%3A24%2Cx%3A24%2Cy%3A22%29%2Cid%3Aeddf7850-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3A%21%2764fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%29%2CgridData%3A%28h%3A14%2Ci%3Abd330ede-2eef-4e2a-8100-22a21abf5038%2Cw%3A24%2Cx%3A24%2Cy%3A32%29%2Cid%3Aff6a21b0-f5ea-11eb-a78e-83aac3c38a60%2CpanelIndex%3Abd330ede-2eef-4e2a-8100-22a21abf5038%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%29%2CgridData%3A%28h%3A9%2Ci%3Ab897d4be-cf83-46fb-a111-c7fbec9ef403%2Cw%3A24%2Cx%3A0%2Cy%3A46%29%2Cid%3A%21%2703071e90-f5eb-11eb-a78e-83aac3c38a60%21%27%2CpanelIndex%3Ab897d4be-cf83-46fb-a111-c7fbec9ef403%2Ctitle%3A%21%27Top%2520products%2520this%2520week%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%2C%28embeddableConfig%3A%28enhancements%3A%28%29%2ChidePanelTitles%3A%21%21f%2CtimeRange%3A%28from%3Anow-2w%2Cto%3Anow-1w%29%29%2CgridData%3A%28h%3A9%2Ci%3Ae0f68f93-30f2-4da7-889a-6cd128a68d3f%2Cw%3A24%2Cx%3A24%2Cy%3A46%29%2Cid%3A%21%2706379e00-f5eb-11eb-a78e-83aac3c38a60%21%27%2CpanelIndex%3Ae0f68f93-30f2-4da7-889a-6cd128a68d3f%2Ctitle%3A%21%27Top%2520products%2520last%2520week%21%27%2Ctype%3Alens%2Cversion%3A%21%278.0.0%21%27%29%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2Ctags%3A%21%21%28%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27%255BeCommerce%255D%2520Revenue%2520Dashboard%21%27%2CviewMode%3Aview%29%27%2Ctitle%3A%27%5BeCommerce%5D%20Revenue%20Dashboard%27%2Cversion%3A%278.0.0%27%29'; const emails = REPORTING_TEST_EMAILS.split(','); const interval = 10; const body = { From 3c41b3fe6397d7e01d81c4aeb90cd33bf7f9a97b Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 12 Aug 2021 14:34:55 -0400 Subject: [PATCH 19/91] [Uptime] Extract o11y page template from page components (#104939) * Refactor synthetics page components to avoid directly rendering o11y page template. * Add tests for synthetics check page components. * TEMP COMMIT. * Implement PR feedback. * Remove file. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../step_detail/step_detail_container.tsx | 127 ++++------------- .../public/lib/helper/spy_use_fetcher.ts | 29 ++++ .../pages/synthetics/step_detail_page.tsx | 129 +++++++++++++++++- .../synthetics/synthetics_checks.test.tsx | 100 ++++++++++++++ .../pages/synthetics/synthetics_checks.tsx | 31 ++--- x-pack/plugins/uptime/public/routes.tsx | 32 +++-- 6 files changed, 318 insertions(+), 130 deletions(-) create mode 100644 x-pack/plugins/uptime/public/lib/helper/spy_use_fetcher.ts create mode 100644 x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx index 9d0555d97cbd4..777491c503fd9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx @@ -7,18 +7,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useEffect, useCallback, useMemo } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { getJourneySteps } from '../../../../state/actions/journey'; -import { journeySelector } from '../../../../state/selectors'; -import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import React from 'react'; import { useMonitorBreadcrumb } from './use_monitor_breadcrumb'; -import { ClientPluginsStart } from '../../../../apps/plugin'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { StepPageTitleContent } from './step_page_title'; -import { StepPageNavigation } from './step_page_nav'; import { WaterfallChartContainer } from './waterfall/waterfall_chart_container'; +import { useStepDetailPage } from '../../../../pages/synthetics/step_detail_page'; export const NO_STEP_DATA = i18n.translate('xpack.uptime.synthetics.stepDetail.noData', { defaultMessage: 'No data could be found for this step', @@ -30,102 +22,31 @@ interface Props { } export const StepDetailContainer: React.FC = ({ checkGroup, stepIndex }) => { - const dispatch = useDispatch(); - const history = useHistory(); - - const [dateFormat] = useUiSetting$('dateFormat'); - - useEffect(() => { - if (checkGroup) { - dispatch(getJourneySteps({ checkGroup, syntheticEventTypes: ['step/end'] })); - } - }, [dispatch, checkGroup]); - - const journeys = useSelector(journeySelector); - const journey = journeys[checkGroup ?? '']; - - const { activeStep, hasPreviousStep, hasNextStep } = useMemo(() => { - return { - hasPreviousStep: stepIndex > 1 ? true : false, - activeStep: journey?.steps?.find((step) => step.synthetics?.step?.index === stepIndex), - hasNextStep: journey && journey.steps && stepIndex < journey.steps.length ? true : false, - }; - }, [stepIndex, journey]); + const { activeStep, journey } = useStepDetailPage(); useMonitorBreadcrumb({ details: journey?.details, activeStep, performanceBreakDownView: true }); - const handleNextStep = useCallback(() => { - history.push(`/journey/${checkGroup}/step/${stepIndex + 1}`); - }, [history, checkGroup, stepIndex]); - - const handlePreviousStep = useCallback(() => { - history.push(`/journey/${checkGroup}/step/${stepIndex - 1}`); - }, [history, checkGroup, stepIndex]); - - const handleNextRun = useCallback(() => { - history.push(`/journey/${journey?.details?.next?.checkGroup}/step/1`); - }, [history, journey?.details?.next?.checkGroup]); - - const handlePreviousRun = useCallback(() => { - history.push(`/journey/${journey?.details?.previous?.checkGroup}/step/1`); - }, [history, journey?.details?.previous?.checkGroup]); - - const { - services: { observability }, - } = useKibana(); - const PageTemplateComponent = observability.navigation.PageTemplate; - return ( - - ) : null, - rightSideItems: journey - ? [ - , - ] - : [], - }} - > - <> - {(!journey || journey.loading) && ( - - - - - - )} - {journey && !activeStep && !journey.loading && ( - - - -

{NO_STEP_DATA}

-
-
-
- )} - {journey && activeStep && !journey.loading && ( - - )} - -
+ <> + {(!journey || journey.loading) && ( + + + + + + )} + {journey && !activeStep && !journey.loading && ( + + + +

{NO_STEP_DATA}

+
+
+
+ )} + {journey && activeStep && !journey.loading && ( + + )} + ); }; diff --git a/x-pack/plugins/uptime/public/lib/helper/spy_use_fetcher.ts b/x-pack/plugins/uptime/public/lib/helper/spy_use_fetcher.ts new file mode 100644 index 0000000000000..c8921361c8f4a --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/helper/spy_use_fetcher.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as observabilityPublic from '../../../../observability/public'; + +jest.mock('../../../../observability/public', () => { + const originalModule = jest.requireActual('../../../../observability/public'); + + return { + ...originalModule, + useFetcher: jest.fn().mockReturnValue({ + data: null, + status: 'success', + }), + useTrackPageview: jest.fn(), + }; +}); + +export function spyOnUseFetcher(payload: unknown) { + jest.spyOn(observabilityPublic, 'useFetcher').mockReturnValue({ + status: observabilityPublic.FETCH_STATUS.SUCCESS, + data: payload, + refetch: () => null, + }); +} diff --git a/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx b/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx index de38d2d663523..fecb6bd1e8454 100644 --- a/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx +++ b/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx @@ -5,11 +5,136 @@ * 2.0. */ -import React from 'react'; -import { useParams } from 'react-router-dom'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; import { useTrackPageview } from '../../../../observability/public'; import { useInitApp } from '../../hooks/use_init_app'; import { StepDetailContainer } from '../../components/monitor/synthetics/step_detail/step_detail_container'; +import { journeySelector } from '../../state/selectors'; +import { JourneyState } from '../../state/reducers/journey'; +import { JourneyStep } from '../../../common/runtime_types/ping/synthetics'; +import { StepPageNavigation } from '../../components/monitor/synthetics/step_detail/step_page_nav'; +import { useUiSetting$ } from '../../../../../../src/plugins/kibana_react/public'; +import { StepPageTitleContent } from '../../components/monitor/synthetics/step_detail/step_page_title'; +import { getJourneySteps } from '../../state/actions/journey'; + +export const useStepDetailPage = (): { + activeStep?: JourneyStep; + checkGroup: string; + handleNextStep: () => void; + handlePreviousStep: () => void; + handleNextRun: () => void; + handlePreviousRun: () => void; + hasNextStep: boolean; + hasPreviousStep: boolean; + journey?: JourneyState; + stepIndex: number; +} => { + const history = useHistory(); + const dispatch = useDispatch(); + + const { checkGroupId: checkGroup, stepIndex: stepIndexString } = useParams<{ + checkGroupId: string; + stepIndex: string; + }>(); + + useEffect(() => { + if (checkGroup) { + dispatch(getJourneySteps({ checkGroup, syntheticEventTypes: ['step/end'] })); + } + }, [dispatch, checkGroup]); + + const stepIndex = Number(stepIndexString); + const journeys = useSelector(journeySelector); + const journey: JourneyState | null = journeys[checkGroup] ?? null; + + const memoized = useMemo( + () => ({ + hasPreviousStep: stepIndex > 1 ? true : false, + activeStep: journey?.steps?.find((step) => step.synthetics?.step?.index === stepIndex), + hasNextStep: journey && journey.steps && stepIndex < journey.steps.length ? true : false, + }), + [journey, stepIndex] + ); + + const handleNextStep = useCallback(() => { + history.push(`/journey/${checkGroup}/step/${stepIndex + 1}`); + }, [history, checkGroup, stepIndex]); + + const handlePreviousStep = useCallback(() => { + history.push(`/journey/${checkGroup}/step/${stepIndex - 1}`); + }, [history, checkGroup, stepIndex]); + + const handleNextRun = useCallback(() => { + history.push(`/journey/${journey?.details?.next?.checkGroup}/step/1`); + }, [history, journey?.details?.next?.checkGroup]); + + const handlePreviousRun = useCallback(() => { + history.push(`/journey/${journey?.details?.previous?.checkGroup}/step/1`); + }, [history, journey?.details?.previous?.checkGroup]); + + return { + checkGroup, + journey, + stepIndex, + ...memoized, + handleNextStep, + handlePreviousStep, + handleNextRun, + handlePreviousRun, + }; +}; + +export const StepDetailPageHeader = () => { + const { activeStep, journey } = useStepDetailPage(); + return <>{journey && activeStep && activeStep.synthetics?.step?.name}; +}; + +export const StepDetailPageRightSideItem = () => { + const [dateFormat] = useUiSetting$('dateFormat'); + + const { journey, handleNextRun, handlePreviousRun } = useStepDetailPage(); + + if (!journey) return null; + + return ( + + ); +}; + +export const StepDetailPageChildren = () => { + const { + activeStep, + hasPreviousStep, + hasNextStep, + handleNextStep, + handlePreviousStep, + journey, + stepIndex, + } = useStepDetailPage(); + + if (!journey || !activeStep) return null; + + return ( + + ); +}; export const StepDetailPage: React.FC = () => { useInitApp(); diff --git a/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.test.tsx b/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.test.tsx new file mode 100644 index 0000000000000..fda203f3ff14b --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { render } from '../../lib/helper/rtl_helpers'; +import { spyOnUseFetcher } from '../../lib/helper/spy_use_fetcher'; +import { + SyntheticsCheckSteps, + SyntheticsCheckStepsPageHeader, + SyntheticsCheckStepsPageRightSideItem, +} from './synthetics_checks'; + +describe('SyntheticsCheckStepsPageHeader component', () => { + it('returns the monitor name', () => { + spyOnUseFetcher({ + details: { + journey: { + monitor: { + name: 'test-name', + id: 'test-id', + }, + }, + }, + }); + const { getByText } = render(); + expect(getByText('test-name')); + }); + + it('returns the monitor ID when no name is provided', () => { + spyOnUseFetcher({ + details: { + journey: { + monitor: { + id: 'test-id', + }, + }, + }, + }); + const { getByText } = render(); + expect(getByText('test-id')); + }); +}); + +describe('SyntheticsCheckStepsPageRightSideItem component', () => { + it('returns null when there are no details', () => { + spyOnUseFetcher(null); + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('renders navigation element if details exist', () => { + spyOnUseFetcher({ + details: { + timestamp: '20031104', + journey: { + monitor: { + name: 'test-name', + id: 'test-id', + }, + }, + }, + }); + const { getByText } = render(); + expect(getByText('Nov 4, 2003 12:00:00 AM')); + expect(getByText('Next check')); + expect(getByText('Previous check')); + }); +}); + +describe('SyntheticsCheckSteps component', () => { + it('renders empty steps list', () => { + const { getByText } = render(); + expect(getByText('0 Steps - all failed or skipped')); + expect(getByText('This journey did not contain any steps.')); + }); + + it('renders steps', () => { + spyOnUseFetcher({ + steps: [ + { + _id: 'step-1', + '@timestamp': '123', + monitor: { + id: 'id', + check_group: 'check-group', + }, + synthetics: { + type: 'step/end', + }, + }, + ], + }); + const { getByText } = render(); + expect(getByText('1 Steps - all failed or skipped')); + }); +}); diff --git a/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx b/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx index fe41e72fa4c48..a03fe674d7c30 100644 --- a/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx +++ b/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../../../../observability/public'; import { useInitApp } from '../../hooks/use_init_app'; import { StepsList } from '../../components/synthetics/check_steps/steps_list'; @@ -14,7 +13,19 @@ import { useCheckSteps } from '../../components/synthetics/check_steps/use_check import { ChecksNavigation } from './checks_navigation'; import { useMonitorBreadcrumb } from '../../components/monitor/synthetics/step_detail/use_monitor_breadcrumb'; import { EmptyJourney } from '../../components/synthetics/empty_journey'; -import { ClientPluginsStart } from '../../apps/plugin'; + +export const SyntheticsCheckStepsPageHeader = () => { + const { details } = useCheckSteps(); + return <>{details?.journey?.monitor.name || details?.journey?.monitor.id}; +}; + +export const SyntheticsCheckStepsPageRightSideItem = () => { + const { details } = useCheckSteps(); + + if (!details) return null; + + return ; +}; export const SyntheticsCheckSteps: React.FC = () => { useInitApp(); @@ -25,22 +36,10 @@ export const SyntheticsCheckSteps: React.FC = () => { useMonitorBreadcrumb({ details, activeStep: details?.journey }); - const { - services: { observability }, - } = useKibana(); - const PageTemplateComponent = observability.navigation.PageTemplate; - return ( - : null, - ], - }} - > + <> {(!steps || steps.length === 0) && !loading && } - + ); }; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index e19f4bd5f93c1..b352b3a6b2732 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -22,7 +22,11 @@ import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages import { CertificatesPage } from './pages/certificates'; import { UptimePage, useUptimeTelemetry } from './hooks'; import { OverviewPageComponent } from './pages/overview'; -import { SyntheticsCheckSteps } from './pages/synthetics/synthetics_checks'; +import { + SyntheticsCheckSteps, + SyntheticsCheckStepsPageHeader, + SyntheticsCheckStepsPageRightSideItem, +} from './pages/synthetics/synthetics_checks'; import { ClientPluginsStart } from './apps/plugin'; import { MonitorPageTitle, MonitorPageTitleContent } from './components/monitor/monitor_title'; import { UptimeDatePicker } from './components/common/uptime_date_picker'; @@ -30,6 +34,11 @@ import { useKibana } from '../../../../src/plugins/kibana_react/public'; import { CertRefreshBtn } from './components/certificates/cert_refresh_btn'; import { CertificateTitle } from './components/certificates/certificate_title'; import { SyntheticsCallout } from './components/overview/synthetics_callout'; +import { + StepDetailPageChildren, + StepDetailPageHeader, + StepDetailPageRightSideItem, +} from './pages/synthetics/step_detail_page'; interface RouteProps { path: string; @@ -37,9 +46,9 @@ interface RouteProps { dataTestSubj: string; title: string; telemetryId: UptimePage; - pageHeader?: { - children?: JSX.Element; + pageHeader: { pageTitle: string | JSX.Element; + children?: JSX.Element; rightSideItems?: JSX.Element[]; }; } @@ -106,6 +115,11 @@ const Routes: RouteProps[] = [ component: StepDetailPage, dataTestSubj: 'uptimeStepDetailPage', telemetryId: UptimePage.StepDetail, + pageHeader: { + children: , + pageTitle: , + rightSideItems: [], + }, }, { title: baseTitle, @@ -113,6 +127,10 @@ const Routes: RouteProps[] = [ component: SyntheticsCheckSteps, dataTestSubj: 'uptimeSyntheticCheckStepsPage', telemetryId: UptimePage.SyntheticCheckStepsPage, + pageHeader: { + pageTitle: , + rightSideItems: [], + }, }, { title: baseTitle, @@ -159,13 +177,9 @@ export const PageRouter: FC = () => {
- {pageHeader ? ( - - - - ) : ( + - )} +
) From fb215edf55516fbc52701cd2e148dbe6281ac294 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 12 Aug 2021 14:46:46 -0400 Subject: [PATCH 20/91] Revert "[Task Manager] [8.0] Remove `xpack.task_manager.index` (#108111)" (#108398) This reverts commit 9dce033408cb0a00b754f996859a3a3171babf02. --- .../advanced/running-elasticsearch.asciidoc | 1 + docs/settings/task-manager-settings.asciidoc | 3 +++ ...task-manager-production-considerations.asciidoc | 2 +- .../resources/base/bin/kibana-docker | 1 + x-pack/plugins/task_manager/server/config.test.ts | 14 ++++++++++++++ x-pack/plugins/task_manager/server/config.ts | 9 +++++++++ x-pack/plugins/task_manager/server/constants.ts | 8 -------- .../server/ephemeral_task_lifecycle.test.ts | 1 + x-pack/plugins/task_manager/server/index.test.ts | 11 +++++++++++ x-pack/plugins/task_manager/server/index.ts | 12 ++++++++++++ .../managed_configuration.test.ts | 1 + .../monitoring/configuration_statistics.test.ts | 1 + .../monitoring/monitoring_stats_stream.test.ts | 1 + x-pack/plugins/task_manager/server/plugin.test.ts | 2 ++ x-pack/plugins/task_manager/server/plugin.ts | 5 ++--- .../task_manager/server/polling_lifecycle.test.ts | 1 + .../task_manager/server/saved_objects/index.ts | 5 ++--- 17 files changed, 63 insertions(+), 15 deletions(-) delete mode 100644 x-pack/plugins/task_manager/server/constants.ts diff --git a/docs/developer/advanced/running-elasticsearch.asciidoc b/docs/developer/advanced/running-elasticsearch.asciidoc index 36f9ee420d41d..324d2af2ed3af 100644 --- a/docs/developer/advanced/running-elasticsearch.asciidoc +++ b/docs/developer/advanced/running-elasticsearch.asciidoc @@ -76,6 +76,7 @@ If many other users will be interacting with your remote cluster, you'll want to [source,bash] ---- kibana.index: '.{YourGitHubHandle}-kibana' +xpack.task_manager.index: '.{YourGitHubHandle}-task-manager-kibana' ---- ==== Running remote clusters diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc index 387d2308aa5e8..fa89b7780e475 100644 --- a/docs/settings/task-manager-settings.asciidoc +++ b/docs/settings/task-manager-settings.asciidoc @@ -22,6 +22,9 @@ Task Manager runs background tasks by polling for work on an interval. You can | `xpack.task_manager.request_capacity` | How many requests can Task Manager buffer before it rejects new requests. Defaults to 1000. +| `xpack.task_manager.index` + | The name of the index used to store task information. Defaults to `.kibana_task_manager`. + | `xpack.task_manager.max_workers` | The maximum number of tasks that this Kibana instance will run simultaneously. Defaults to 10. Starting in 8.0, it will not be possible to set the value greater than 100. diff --git a/docs/user/production-considerations/task-manager-production-considerations.asciidoc b/docs/user/production-considerations/task-manager-production-considerations.asciidoc index 36745b913544b..17eae59ff2f9c 100644 --- a/docs/user/production-considerations/task-manager-production-considerations.asciidoc +++ b/docs/user/production-considerations/task-manager-production-considerations.asciidoc @@ -12,7 +12,7 @@ This has three major benefits: [IMPORTANT] ============================================== -Task definitions for alerts and actions are stored in the index `.kibana_task_manager`. +Task definitions for alerts and actions are stored in the index specified by <>. The default is `.kibana_task_manager`. You must have at least one replica of this index for production deployments. diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index e65c5542cce7e..e2d81c5ae1752 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -395,6 +395,7 @@ kibana_vars=( xpack.spaces.enabled xpack.spaces.maxSpaces xpack.task_manager.enabled + xpack.task_manager.index xpack.task_manager.max_attempts xpack.task_manager.max_poll_inactivity_cycles xpack.task_manager.max_workers diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index e237f5592419b..14d95e3fd2226 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -17,6 +17,7 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, + "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, @@ -41,6 +42,17 @@ describe('config validation', () => { `); }); + test('the ElastiSearch Tasks index cannot be used for task manager', () => { + const config: Record = { + index: '.tasks', + }; + expect(() => { + configSchema.validate(config); + }).toThrowErrorMatchingInlineSnapshot( + `"[index]: \\".tasks\\" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager"` + ); + }); + test('the required freshness of the monitored stats config must always be less-than-equal to the poll interval', () => { const config: Record = { monitored_stats_required_freshness: 100, @@ -61,6 +73,7 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, + "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, @@ -103,6 +116,7 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, + "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index 7c541cd24cefd..9b4f4856bf8a9 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -65,6 +65,15 @@ export const configSchema = schema.object( defaultValue: 1000, min: 1, }), + /* The name of the index used to store task information. */ + index: schema.string({ + defaultValue: '.kibana_task_manager', + validate: (val) => { + if (val.toLowerCase() === '.tasks') { + return `"${val}" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager`; + } + }, + }), /* The maximum number of tasks that this Kibana instance will run simultaneously. */ max_workers: schema.number({ defaultValue: DEFAULT_MAX_WORKERS, diff --git a/x-pack/plugins/task_manager/server/constants.ts b/x-pack/plugins/task_manager/server/constants.ts deleted file mode 100644 index 9334fbede3176..0000000000000 --- a/x-pack/plugins/task_manager/server/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const TASK_MANAGER_INDEX = '.kibana_task_manager'; diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts index 859f242f2f0a6..182e7cd5bcabf 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts @@ -40,6 +40,7 @@ describe('EphemeralTaskLifecycle', () => { config: { enabled: true, max_workers: 10, + index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/index.test.ts b/x-pack/plugins/task_manager/server/index.test.ts index 74d86c31e1bd1..8eb98c39a2ccd 100644 --- a/x-pack/plugins/task_manager/server/index.test.ts +++ b/x-pack/plugins/task_manager/server/index.test.ts @@ -31,6 +31,17 @@ const applyTaskManagerDeprecations = (settings: Record = {}) => }; describe('deprecations', () => { + ['.foo', '.kibana_task_manager'].forEach((index) => { + it('logs a warning if index is set', () => { + const { messages } = applyTaskManagerDeprecations({ index }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.task_manager.index\\" is deprecated. Multitenancy by changing \\"kibana.index\\" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details", + ] + `); + }); + }); + it('logs a warning if max_workers is over limit', () => { const { messages } = applyTaskManagerDeprecations({ max_workers: 1000 }); expect(messages).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 067082955b3b1..cc4217f41c5ef 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -41,6 +41,18 @@ export const config: PluginConfigDescriptor = { deprecations: () => [ (settings, fromPath, addDeprecation) => { const taskManager = get(settings, fromPath); + if (taskManager?.index) { + addDeprecation({ + documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', + message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, + correctiveActions: { + manualSteps: [ + `If you rely on this setting to achieve multitenancy you should use Spaces, cross-cluster replication, or cross-cluster search instead.`, + `To migrate to Spaces, we encourage using saved object management to export your saved objects from a tenant into the default tenant in a space.`, + ], + }, + }); + } if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { addDeprecation({ message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`, diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index ce49466ff387c..496c0138cb1e5 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -31,6 +31,7 @@ describe('managed configuration', () => { const context = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, + index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index e63beee7201fe..82a111305927f 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -15,6 +15,7 @@ describe('Configuration Statistics Aggregator', () => { const configuration: TaskManagerConfig = { enabled: true, max_workers: 10, + index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index d59d446144632..50d4b6af9a4cf 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -19,6 +19,7 @@ describe('createMonitoringStatsStream', () => { const configuration: TaskManagerConfig = { enabled: true, max_workers: 10, + index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index de21b653823c9..dff94259dbe62 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -18,6 +18,7 @@ describe('TaskManagerPlugin', () => { const pluginInitializerContext = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, + index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, @@ -57,6 +58,7 @@ describe('TaskManagerPlugin', () => { const pluginInitializerContext = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, + index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index c41bc8109ef4c..3d3d180fc0665 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -31,7 +31,6 @@ import { createMonitoringStats, MonitoringStats } from './monitoring'; import { EphemeralTaskLifecycle } from './ephemeral_task_lifecycle'; import { EphemeralTask } from './task'; import { registerTaskManagerUsageCollector } from './usage'; -import { TASK_MANAGER_INDEX } from './constants'; export type TaskManagerSetupContract = { /** @@ -115,7 +114,7 @@ export class TaskManagerPlugin } return { - index: TASK_MANAGER_INDEX, + index: this.config.index, addMiddleware: (middleware: Middleware) => { this.assertStillInSetup('add Middleware'); this.middleware = addMiddlewareToChain(this.middleware, middleware); @@ -135,7 +134,7 @@ export class TaskManagerPlugin serializer, savedObjectsRepository, esClient: elasticsearch.createClient('taskManager').asInternalUser, - index: TASK_MANAGER_INDEX, + index: this.config!.index, definitions: this.definitions, taskManagerId: `kibana:${this.taskManagerId!}`, }); diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index 1420a81b2dcaa..aad03951bbb9b 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -38,6 +38,7 @@ describe('TaskPollingLifecycle', () => { config: { enabled: true, max_workers: 10, + index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts index e98a02b220d58..d2d079c7747b1 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -11,7 +11,6 @@ import mappings from './mappings.json'; import { migrations } from './migrations'; import { TaskManagerConfig } from '../config.js'; import { getOldestIdleActionTask } from '../queries/oldest_idle_action_task'; -import { TASK_MANAGER_INDEX } from '../constants'; export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, @@ -24,11 +23,11 @@ export function setupSavedObjects( convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id; ctx._source.remove("kibana")`, mappings: mappings.task as SavedObjectsTypeMappingDefinition, migrations, - indexPattern: TASK_MANAGER_INDEX, + indexPattern: config.index, excludeOnUpgrade: async ({ readonlyEsClient }) => { const oldestNeededActionParams = await getOldestIdleActionTask( readonlyEsClient, - TASK_MANAGER_INDEX + config.index ); // Delete all action tasks that have failed and are no longer needed From e91baea5dca6edd39208c338890c4b5a1d0427a8 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 12 Aug 2021 21:23:33 +0200 Subject: [PATCH 21/91] [Data][Es Query] Use ES types instead of DslQuery (#108290) * es-query types * jest and lint * cc * options * type * type --- ...ns-data-public.aggconfigs._constructor_.md | 4 +- ...plugins-data-public.aggconfigserialized.md | 4 +- ...bana-plugin-plugins-data-public.eskuery.md | 4 +- ...bana-plugin-plugins-data-public.esquery.md | 7 +--- ...lugin-plugins-data-public.filtermanager.md | 2 +- ...ins-data-public.filtermanager.telemetry.md | 2 +- ...bana-plugin-plugins-data-server.eskuery.md | 2 +- ...bana-plugin-plugins-data-server.esquery.md | 7 +--- .../public/search_sessions/app.tsx | 1 - .../src/es_query/build_es_query.ts | 9 +++-- .../src/es_query/decorate_query.ts | 8 ++-- .../kbn-es-query/src/es_query/es_query_dsl.ts | 38 ------------------- .../kbn-es-query/src/es_query/from_filters.ts | 19 +++++----- .../kbn-es-query/src/es_query/from_kuery.ts | 7 ++-- .../kbn-es-query/src/es_query/from_lucene.ts | 6 ++- packages/kbn-es-query/src/es_query/index.ts | 2 +- .../src/es_query/lucene_string_to_dsl.ts | 6 ++- .../src/es_query/migrate_filter.ts | 2 +- packages/kbn-es-query/src/es_query/types.ts | 7 ++++ .../src/filters/helpers/meta_filter.ts | 2 +- packages/kbn-es-query/src/kuery/ast/ast.ts | 9 +++-- packages/kbn-es-query/src/kuery/types.ts | 4 +- src/plugins/data/public/public.api.md | 9 +---- .../lib/map_and_flatten_filters.ts | 2 +- .../query/filter_manager/lib/map_filter.ts | 2 +- src/plugins/data/server/server.api.md | 9 +---- .../vis_type_vega/public/data_model/types.ts | 9 ++--- .../log_entries/log_entries.ts | 6 +-- .../log_stream/use_fetch_log_entries_after.ts | 3 +- .../use_fetch_log_entries_before.ts | 3 +- .../server/services/items/find_list_item.ts | 2 - .../lists/server/services/lists/find_list.ts | 2 - .../server/services/utils/get_query_filter.ts | 8 ++-- .../services/utils/get_search_after_scroll.ts | 1 - x-pack/plugins/ml/common/types/es_client.ts | 7 +++- .../explorer_query_bar/explorer_query_bar.tsx | 6 +-- .../reducers/explorer_reducer/state.ts | 2 +- .../open_in_anomaly_explorer_action.tsx | 3 +- x-pack/plugins/osquery/common/typed_json.ts | 9 +---- .../security_solution/common/typed_json.ts | 10 +---- .../signals/threat_mapping/get_threat_list.ts | 2 - .../lib/detection_engine/signals/types.ts | 9 +---- x-pack/plugins/timelines/common/typed_json.ts | 10 +---- 43 files changed, 99 insertions(+), 167 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md index c4d09001087de..9111941b368ee 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md @@ -13,7 +13,7 @@ constructor(indexPattern: IndexPattern, configStates: Pick & Pick<{ type: string | IAggType; @@ -27,6 +27,6 @@ constructor(indexPattern: IndexPattern, configStates: PickIndexPattern | | -| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("@kbn/common-utils").SerializableRecord | undefined;
schema?: string | undefined;
}, "schema" | "enabled" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined | | +| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("@kbn/utility-types").SerializableRecord | undefined;
schema?: string | undefined;
}, "schema" | "enabled" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined | | | opts | AggConfigsOptions | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigserialized.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigserialized.md index 9660a15d94a69..631569464e176 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigserialized.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigserialized.md @@ -13,7 +13,7 @@ export declare type AggConfigSerialized = Ensure<{ type: string; enabled?: boolean; id?: string; - params?: {} | SerializableState; + params?: {} | SerializableRecord; schema?: string; -}, SerializableState>; +}, SerializableRecord>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md index 3b768404aab95..c25cd70e99b4f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md @@ -14,7 +14,7 @@ ```typescript esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; - fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; - toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + fromKueryExpression: (expression: string | import("@elastic/elasticsearch/api/types").QueryDslQueryContainer, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md index 2a26b009d7447..0ffdf8c98b920 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md @@ -15,12 +15,7 @@ esQuery: { buildEsQuery: typeof import("@kbn/es-query").buildEsQuery; getEsQueryConfig: typeof getEsQueryConfig; - buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => { - must: never[]; - filter: import("@kbn/es-query").Filter[]; - should: never[]; - must_not: import("@kbn/es-query").Filter[]; - }; + buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => import("@kbn/es-query").BoolQuery; luceneStringToDsl: typeof import("@kbn/es-query").luceneStringToDsl; decorateQuery: typeof import("@kbn/es-query").decorateQuery; } diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md index 7baa23fffe0d3..7cfc8c4e48805 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md @@ -24,7 +24,7 @@ export declare class FilterManager implements PersistableStateService | [getAllMigrations](./kibana-plugin-plugins-data-public.filtermanager.getallmigrations.md) | | () => {} | | | [inject](./kibana-plugin-plugins-data-public.filtermanager.inject.md) | | any | | | [migrateToLatest](./kibana-plugin-plugins-data-public.filtermanager.migratetolatest.md) | | any | | -| [telemetry](./kibana-plugin-plugins-data-public.filtermanager.telemetry.md) | | (filters: import("@kbn/common-utils").SerializableRecord, collector: unknown) => {} | | +| [telemetry](./kibana-plugin-plugins-data-public.filtermanager.telemetry.md) | | (filters: import("@kbn/utility-types").SerializableRecord, collector: unknown) => {} | | ## Methods diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md index df5b4ea0a26c8..0eeb026abf2e1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md @@ -7,5 +7,5 @@ Signature: ```typescript -telemetry: (filters: import("@kbn/common-utils").SerializableRecord, collector: unknown) => {}; +telemetry: (filters: import("@kbn/utility-types").SerializableRecord, collector: unknown) => {}; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md index f0261648e32ab..620a547d30245 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md @@ -9,7 +9,7 @@ ```typescript esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; - fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; + fromKueryExpression: (expression: string | import("@elastic/elasticsearch/api/types").QueryDslQueryContainer, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md index 8dfea00081d89..38cad914e72d0 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md @@ -8,12 +8,7 @@ ```typescript esQuery: { - buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => { - must: never[]; - filter: import("@kbn/es-query").Filter[]; - should: never[]; - must_not: import("@kbn/es-query").Filter[]; - }; + buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => import("@kbn/es-query").BoolQuery; getEsQueryConfig: typeof getEsQueryConfig; buildEsQuery: typeof import("@kbn/es-query").buildEsQuery; } diff --git a/examples/search_examples/public/search_sessions/app.tsx b/examples/search_examples/public/search_sessions/app.tsx index 7fdf91537c977..173cf91cd9c71 100644 --- a/examples/search_examples/public/search_sessions/app.tsx +++ b/examples/search_examples/public/search_sessions/app.tsx @@ -702,7 +702,6 @@ function doSearch( const startTs = performance.now(); // Submit the search request using the `data.search` service. - // @ts-expect-error request.params is incompatible. Filter is not assignable to QueryDslQueryContainer return data.search .search(req, { sessionId }) .pipe( diff --git a/packages/kbn-es-query/src/es_query/build_es_query.ts b/packages/kbn-es-query/src/es_query/build_es_query.ts index c01b11f580ba6..42fa26ac50a95 100644 --- a/packages/kbn-es-query/src/es_query/build_es_query.ts +++ b/packages/kbn-es-query/src/es_query/build_es_query.ts @@ -7,11 +7,12 @@ */ import { groupBy, has, isEqual } from 'lodash'; +import { SerializableRecord } from '@kbn/utility-types'; import { buildQueryFromKuery } from './from_kuery'; import { buildQueryFromFilters } from './from_filters'; import { buildQueryFromLucene } from './from_lucene'; import { Filter, Query } from '../filters'; -import { IndexPatternBase } from './types'; +import { BoolQuery, IndexPatternBase } from './types'; import { KueryQueryOptions } from '../kuery'; /** @@ -20,7 +21,7 @@ import { KueryQueryOptions } from '../kuery'; */ export type EsQueryConfig = KueryQueryOptions & { allowLeadingWildcards: boolean; - queryStringOptions: Record; + queryStringOptions: SerializableRecord; ignoreFilterIfFieldNotInIndex: boolean; }; @@ -49,11 +50,11 @@ export function buildEsQuery( queryStringOptions: {}, ignoreFilterIfFieldNotInIndex: false, } -) { +): { bool: BoolQuery } { queries = Array.isArray(queries) ? queries : [queries]; filters = Array.isArray(filters) ? filters : [filters]; - const validQueries = queries.filter((query: any) => has(query, 'query')); + const validQueries = queries.filter((query) => has(query, 'query')); const queriesByLanguage = groupBy(validQueries, 'language'); const kueryQuery = buildQueryFromKuery( indexPattern, diff --git a/packages/kbn-es-query/src/es_query/decorate_query.ts b/packages/kbn-es-query/src/es_query/decorate_query.ts index b6623b9b1946c..e5bcf01a45915 100644 --- a/packages/kbn-es-query/src/es_query/decorate_query.ts +++ b/packages/kbn-es-query/src/es_query/decorate_query.ts @@ -6,9 +6,11 @@ * Side Public License, v 1. */ +import { estypes } from '@elastic/elasticsearch'; +import { SerializableRecord } from '@kbn/utility-types'; import { extend, defaults } from 'lodash'; import { getTimeZoneFromSettings } from '../utils'; -import { DslQuery, isEsQueryString } from './es_query_dsl'; +import { isEsQueryString } from './es_query_dsl'; /** * Decorate queries with default parameters @@ -21,8 +23,8 @@ import { DslQuery, isEsQueryString } from './es_query_dsl'; */ export function decorateQuery( - query: DslQuery, - queryStringOptions: Record | string, + query: estypes.QueryDslQueryContainer, + queryStringOptions: SerializableRecord | string, dateFormatTZ?: string ) { if (isEsQueryString(query)) { diff --git a/packages/kbn-es-query/src/es_query/es_query_dsl.ts b/packages/kbn-es-query/src/es_query/es_query_dsl.ts index 6cff8b0ff47c7..90b234c0e29d7 100644 --- a/packages/kbn-es-query/src/es_query/es_query_dsl.ts +++ b/packages/kbn-es-query/src/es_query/es_query_dsl.ts @@ -8,26 +8,6 @@ import { has } from 'lodash'; -export interface DslRangeQuery { - range: { - [name: string]: { - gte: number; - lte: number; - format: string; - }; - }; -} - -export interface DslMatchQuery { - match: { - [name: string]: { - query: string; - operator: string; - zero_terms_query: string; - }; - }; -} - export interface DslQueryStringQuery { query_string: { query: string; @@ -35,24 +15,6 @@ export interface DslQueryStringQuery { }; } -export interface DslMatchAllQuery { - match_all: Record; -} - -export interface DslTermQuery { - term: Record; -} - -/** - * @public - */ -export type DslQuery = - | DslRangeQuery - | DslMatchQuery - | DslQueryStringQuery - | DslMatchAllQuery - | DslTermQuery; - /** @internal */ export const isEsQueryString = (query: any): query is DslQueryStringQuery => has(query, 'query_string.query'); diff --git a/packages/kbn-es-query/src/es_query/from_filters.ts b/packages/kbn-es-query/src/es_query/from_filters.ts index 94def4008a2bc..ea2ee18442703 100644 --- a/packages/kbn-es-query/src/es_query/from_filters.ts +++ b/packages/kbn-es-query/src/es_query/from_filters.ts @@ -7,10 +7,11 @@ */ import { isUndefined } from 'lodash'; +import { estypes } from '@elastic/elasticsearch'; import { migrateFilter } from './migrate_filter'; import { filterMatchesIndex } from './filter_matches_index'; import { Filter, cleanFilter, isFilterDisabled } from '../filters'; -import { IndexPatternBase } from './types'; +import { BoolQuery, IndexPatternBase } from './types'; import { handleNestedFilter } from './handle_nested_filter'; /** @@ -33,20 +34,19 @@ const filterNegate = (reverse: boolean) => (filter: Filter) => { * @param {Object} filter - The filter to translate * @return {Object} the query version of that filter */ -const translateToQuery = (filter: Filter) => { - if (!filter) return; - +const translateToQuery = (filter: Partial): estypes.QueryDslQueryContainer => { if (filter.query) { return filter.query; } - return filter; + // TODO: investigate what's going on here! What does this mean for filters that don't have a query! + return filter as estypes.QueryDslQueryContainer; }; /** * @param filters * @param indexPattern - * @param ignoreFilterIfFieldNotInIndex by default filters that use fields that can't be found in the specified index pattern are not applied. Set this to true if you want to apply them any way. + * @param ignoreFilterIfFieldNotInIndex by default filters that use fields that can't be found in the specified index pattern are not applied. Set this to true if you want to apply them anyway. * @returns An EQL query * * @public @@ -55,11 +55,12 @@ export const buildQueryFromFilters = ( filters: Filter[] = [], indexPattern: IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex: boolean = false -) => { +): BoolQuery => { filters = filters.filter((filter) => filter && !isFilterDisabled(filter)); const filtersToESQueries = (negate: boolean) => { return filters + .filter((f) => !!f) .filter(filterNegate(negate)) .filter( (filter) => !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern) @@ -68,8 +69,8 @@ export const buildQueryFromFilters = ( return migrateFilter(filter, indexPattern); }) .map((filter) => handleNestedFilter(filter, indexPattern)) - .map(translateToQuery) - .map(cleanFilter); + .map(cleanFilter) + .map(translateToQuery); }; return { diff --git a/packages/kbn-es-query/src/es_query/from_kuery.ts b/packages/kbn-es-query/src/es_query/from_kuery.ts index bf66057e49327..949f9691e9e6d 100644 --- a/packages/kbn-es-query/src/es_query/from_kuery.ts +++ b/packages/kbn-es-query/src/es_query/from_kuery.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ +import { SerializableRecord } from '@kbn/utility-types'; import { Query } from '../filters'; import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery'; -import { IndexPatternBase } from './types'; +import { BoolQuery, IndexPatternBase } from './types'; /** @internal */ export function buildQueryFromKuery( @@ -17,7 +18,7 @@ export function buildQueryFromKuery( allowLeadingWildcards: boolean = false, dateFormatTZ?: string, filtersInMustClause: boolean = false -) { +): BoolQuery { const queryASTs = queries.map((query) => { return fromKueryExpression(query.query, { allowLeadingWildcards }); }); @@ -28,7 +29,7 @@ export function buildQueryFromKuery( function buildQuery( indexPattern: IndexPatternBase | undefined, queryASTs: KueryNode[], - config: Record = {} + config: SerializableRecord = {} ) { const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs); const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern, config); diff --git a/packages/kbn-es-query/src/es_query/from_lucene.ts b/packages/kbn-es-query/src/es_query/from_lucene.ts index ef4becd1d1584..d00614b31347f 100644 --- a/packages/kbn-es-query/src/es_query/from_lucene.ts +++ b/packages/kbn-es-query/src/es_query/from_lucene.ts @@ -6,16 +6,18 @@ * Side Public License, v 1. */ +import { SerializableRecord } from '@kbn/utility-types'; import { Query } from '..'; import { decorateQuery } from './decorate_query'; import { luceneStringToDsl } from './lucene_string_to_dsl'; +import { BoolQuery } from './types'; /** @internal */ export function buildQueryFromLucene( queries: Query[], - queryStringOptions: Record, + queryStringOptions: SerializableRecord, dateFormatTZ?: string -) { +): BoolQuery { const combinedQueries = (queries || []).map((query) => { const queryDsl = luceneStringToDsl(query.query); diff --git a/packages/kbn-es-query/src/es_query/index.ts b/packages/kbn-es-query/src/es_query/index.ts index beba50f50dd81..6e4a58fbe96c3 100644 --- a/packages/kbn-es-query/src/es_query/index.ts +++ b/packages/kbn-es-query/src/es_query/index.ts @@ -10,4 +10,4 @@ export { buildEsQuery, EsQueryConfig } from './build_es_query'; export { buildQueryFromFilters } from './from_filters'; export { luceneStringToDsl } from './lucene_string_to_dsl'; export { decorateQuery } from './decorate_query'; -export { IndexPatternBase, IndexPatternFieldBase, IFieldSubType } from './types'; +export { IndexPatternBase, IndexPatternFieldBase, IFieldSubType, BoolQuery } from './types'; diff --git a/packages/kbn-es-query/src/es_query/lucene_string_to_dsl.ts b/packages/kbn-es-query/src/es_query/lucene_string_to_dsl.ts index 2e4eb5ab7f7c4..91a912a5da0e3 100644 --- a/packages/kbn-es-query/src/es_query/lucene_string_to_dsl.ts +++ b/packages/kbn-es-query/src/es_query/lucene_string_to_dsl.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ +import { estypes } from '@elastic/elasticsearch'; import { isString } from 'lodash'; -import { DslQuery } from './es_query_dsl'; /** * @@ -16,7 +16,9 @@ import { DslQuery } from './es_query_dsl'; * * @public */ -export function luceneStringToDsl(query: string | any): DslQuery { +export function luceneStringToDsl( + query: string | estypes.QueryDslQueryContainer +): estypes.QueryDslQueryContainer { if (isString(query)) { if (query.trim() === '') { return { match_all: {} }; diff --git a/packages/kbn-es-query/src/es_query/migrate_filter.ts b/packages/kbn-es-query/src/es_query/migrate_filter.ts index 5edab3e042f5c..8fc0278433645 100644 --- a/packages/kbn-es-query/src/es_query/migrate_filter.ts +++ b/packages/kbn-es-query/src/es_query/migrate_filter.ts @@ -23,7 +23,7 @@ export interface DeprecatedMatchPhraseFilter extends Filter { }; } -function isDeprecatedMatchPhraseFilter(filter: any): filter is DeprecatedMatchPhraseFilter { +function isDeprecatedMatchPhraseFilter(filter: Filter): filter is DeprecatedMatchPhraseFilter { const fieldName = filter.query && filter.query.match && Object.keys(filter.query.match)[0]; return Boolean(fieldName && get(filter, ['query', 'match', fieldName, 'type']) === 'phrase'); diff --git a/packages/kbn-es-query/src/es_query/types.ts b/packages/kbn-es-query/src/es_query/types.ts index d68d9e4a4da22..333536a5f3ecd 100644 --- a/packages/kbn-es-query/src/es_query/types.ts +++ b/packages/kbn-es-query/src/es_query/types.ts @@ -49,3 +49,10 @@ export interface IndexPatternBase { id?: string; title?: string; } + +export interface BoolQuery { + must: estypes.QueryDslQueryContainer[]; + must_not: estypes.QueryDslQueryContainer[]; + filter: estypes.QueryDslQueryContainer[]; + should: estypes.QueryDslQueryContainer[]; +} diff --git a/packages/kbn-es-query/src/filters/helpers/meta_filter.ts b/packages/kbn-es-query/src/filters/helpers/meta_filter.ts index 61b89d45d1962..1406c979bc549 100644 --- a/packages/kbn-es-query/src/filters/helpers/meta_filter.ts +++ b/packages/kbn-es-query/src/filters/helpers/meta_filter.ts @@ -135,4 +135,4 @@ export const isFilters = (x: unknown): x is Filter[] => * * @public */ -export const cleanFilter = (filter: Filter): Filter => omit(filter, ['meta', '$state']) as Filter; +export const cleanFilter = (filter: Filter): Partial => omit(filter, ['meta', '$state']); diff --git a/packages/kbn-es-query/src/kuery/ast/ast.ts b/packages/kbn-es-query/src/kuery/ast/ast.ts index 030b5a8f1c29a..826fa194f1b30 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.ts @@ -7,15 +7,16 @@ */ import { JsonObject } from '@kbn/utility-types'; +import { estypes } from '@elastic/elasticsearch'; import { nodeTypes } from '../node_types/index'; import { KQLSyntaxError } from '../kuery_syntax_error'; -import { KueryNode, DslQuery, KueryParseOptions } from '../types'; +import { KueryNode, KueryParseOptions } from '../types'; import { parse as parseKuery } from '../grammar'; import { IndexPatternBase } from '../..'; const fromExpression = ( - expression: string | DslQuery, + expression: string | estypes.QueryDslQueryContainer, parseOptions: Partial = {}, parse: Function = parseKuery ): KueryNode => { @@ -27,7 +28,7 @@ const fromExpression = ( }; export const fromLiteralExpression = ( - expression: string | DslQuery, + expression: string | estypes.QueryDslQueryContainer, parseOptions: Partial = {} ): KueryNode => { return fromExpression( @@ -41,7 +42,7 @@ export const fromLiteralExpression = ( }; export const fromKueryExpression = ( - expression: string | DslQuery, + expression: string | estypes.QueryDslQueryContainer, parseOptions: Partial = {} ): KueryNode => { try { diff --git a/packages/kbn-es-query/src/kuery/types.ts b/packages/kbn-es-query/src/kuery/types.ts index 59c48f21425bc..656e06e712079 100644 --- a/packages/kbn-es-query/src/kuery/types.ts +++ b/packages/kbn-es-query/src/kuery/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { estypes } from '@elastic/elasticsearch'; import { NodeTypes } from './node_types'; /** @public */ @@ -15,10 +16,9 @@ export interface KueryNode { } /** - * TODO: Replace with real type * @public */ -export type DslQuery = any; +export type DslQuery = estypes.QueryDslQueryContainer; /** @internal */ export interface KueryParseOptions { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 2fee05760186b..995a1e7a908c5 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -791,7 +791,7 @@ export const esFilters: { // @public @deprecated (undocumented) export const esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; - fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; + fromKueryExpression: (expression: string | import("@elastic/elasticsearch/api/types").QueryDslQueryContainer, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; }; @@ -801,12 +801,7 @@ export const esKuery: { export const esQuery: { buildEsQuery: typeof import("@kbn/es-query").buildEsQuery; getEsQueryConfig: typeof getEsQueryConfig; - buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => { - must: never[]; - filter: import("@kbn/es-query").Filter[]; - should: never[]; - must_not: import("@kbn/es-query").Filter[]; - }; + buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => import("@kbn/es-query").BoolQuery; luceneStringToDsl: typeof import("@kbn/es-query").luceneStringToDsl; decorateQuery: typeof import("@kbn/es-query").decorateQuery; }; diff --git a/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.ts b/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.ts index 4e871ad7263f6..73894a4b9ab63 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.ts @@ -7,8 +7,8 @@ */ import { compact, flatten } from 'lodash'; +import { Filter } from '@kbn/es-query'; import { mapFilter } from './map_filter'; -import { Filter } from '../../../../common'; export const mapAndFlattenFilters = (filters: Filter[]) => { return compact(flatten(filters)).map((item: Filter) => mapFilter(item)); diff --git a/src/plugins/data/public/query/filter_manager/lib/map_filter.ts b/src/plugins/data/public/query/filter_manager/lib/map_filter.ts index e017775003ec9..249c7bf47b8fb 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_filter.ts @@ -8,6 +8,7 @@ import { reduceRight } from 'lodash'; +import { Filter } from '@kbn/es-query'; import { mapSpatialFilter } from './mappers/map_spatial_filter'; import { mapMatchAll } from './mappers/map_match_all'; import { mapPhrase } from './mappers/map_phrase'; @@ -20,7 +21,6 @@ import { mapGeoBoundingBox } from './mappers/map_geo_bounding_box'; import { mapGeoPolygon } from './mappers/map_geo_polygon'; import { mapDefault } from './mappers/map_default'; import { generateMappingChain } from './generate_mapping_chain'; -import { Filter } from '../../../../common'; export function mapFilter(filter: Filter) { /** Mappers **/ diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 121cd8ebc0af7..c1f22d1be1d01 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -124,7 +124,7 @@ export const esFilters: { // @public (undocumented) export const esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; - fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; + fromKueryExpression: (expression: string | import("@elastic/elasticsearch/api/types").QueryDslQueryContainer, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; }; @@ -132,12 +132,7 @@ export const esKuery: { // // @public (undocumented) export const esQuery: { - buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => { - must: never[]; - filter: import("@kbn/es-query").Filter[]; - should: never[]; - must_not: import("@kbn/es-query").Filter[]; - }; + buildQueryFromFilters: (filters: import("@kbn/es-query").Filter[] | undefined, indexPattern: import("@kbn/es-query").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean | undefined) => import("@kbn/es-query").BoolQuery; getEsQueryConfig: typeof getEsQueryConfig; buildEsQuery: typeof import("@kbn/es-query").buildEsQuery; }; diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 4d57ccf402a9a..75b1132176d67 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -7,7 +7,6 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { DslQuery, Filter } from '@kbn/es-query'; import { Assign } from '@kbn/utility-types'; import { Spec } from 'vega'; import { EsQueryParser } from './es_query_parser'; @@ -143,10 +142,10 @@ export interface TimeBucket { export interface Bool { [index: string]: any; bool?: Bool; - must?: DslQuery[]; - filter?: Filter[]; - should?: Filter[]; - must_not?: Filter[]; + must?: estypes.QueryDslQueryContainer[]; + filter?: estypes.QueryDslQueryContainer[]; + should?: estypes.QueryDslQueryContainer[]; + must_not?: estypes.QueryDslQueryContainer[]; } export interface Query { diff --git a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts index b9841abad6ab5..cc6d3fbe585e0 100644 --- a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts +++ b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { DslQuery } from '@kbn/es-query'; +import { estypes } from '@elastic/elasticsearch'; import { logSourceColumnConfigurationRT } from '../../log_sources/log_source_configuration'; import { logEntryAfterCursorRT, @@ -14,7 +14,7 @@ import { logEntryCursorRT, logEntryRT, } from '../../log_entry'; -import { JsonObject, jsonObjectRT } from '../../typed_json'; +import { jsonObjectRT } from '../../typed_json'; import { searchStrategyErrorRT } from '../common/errors'; export const LOG_ENTRIES_SEARCH_STRATEGY = 'infra-log-entries'; @@ -51,7 +51,7 @@ export const logEntriesSearchRequestParamsRT = rt.union([ export type LogEntriesSearchRequestParams = rt.TypeOf; -export type LogEntriesSearchRequestQuery = JsonObject | DslQuery; +export type LogEntriesSearchRequestQuery = estypes.QueryDslQueryContainer; export const logEntriesSearchResponsePayloadRT = rt.intersection([ rt.type({ diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_after.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_after.ts index 5455029afa1a7..e41bc07dfd364 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_after.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_after.ts @@ -8,6 +8,7 @@ import { useCallback } from 'react'; import { Observable } from 'rxjs'; import { exhaustMap } from 'rxjs/operators'; +import { JsonObject } from '@kbn/utility-types'; import { IKibanaSearchRequest } from '../../../../../../../src/plugins/data/public'; import { LogSourceColumnConfiguration } from '../../../../common/log_sources'; import { LogEntryAfterCursor } from '../../../../common/log_entry'; @@ -55,7 +56,7 @@ export const useLogEntriesAfterRequest = ({ columns: columnOverrides, endTimestamp: params?.extendTo ?? endTimestamp, highlightPhrase, - query, + query: query as JsonObject, size: params.size, sourceId, startTimestamp, diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_before.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_before.ts index ef57a16ecbeef..c9a7dab168200 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_before.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_before.ts @@ -8,6 +8,7 @@ import { useCallback } from 'react'; import { Observable } from 'rxjs'; import { exhaustMap } from 'rxjs/operators'; +import { JsonObject } from '@kbn/utility-types'; import { IKibanaSearchRequest } from '../../../../../../../src/plugins/data/public'; import { LogSourceColumnConfiguration } from '../../../../common/log_sources'; import { LogEntryBeforeCursor } from '../../../../common/log_entry'; @@ -57,7 +58,7 @@ export const useLogEntriesBeforeRequest = ({ columns: columnOverrides, endTimestamp, highlightPhrase, - query, + query: query as JsonObject, size: params.size, sourceId, startTimestamp: params.extendTo ?? startTimestamp, diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.ts b/x-pack/plugins/lists/server/services/items/find_list_item.ts index 29bfdf8bf1d13..4ab8c11cc5b2f 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.ts @@ -76,7 +76,6 @@ export const findListItem = async ({ const { body: respose } = await esClient.count({ body: { - // @ts-expect-error GetQueryFilterReturn is not assignable to QueryDslQueryContainer query, }, ignore_unavailable: true, @@ -89,7 +88,6 @@ export const findListItem = async ({ // to explicitly define the type . const { body: response } = await esClient.search({ body: { - // @ts-expect-error GetQueryFilterReturn is not assignable to QueryDslQueryContainer query, search_after: scroll.searchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), diff --git a/x-pack/plugins/lists/server/services/lists/find_list.ts b/x-pack/plugins/lists/server/services/lists/find_list.ts index ae28428d80a5c..ed46c053cda9e 100644 --- a/x-pack/plugins/lists/server/services/lists/find_list.ts +++ b/x-pack/plugins/lists/server/services/lists/find_list.ts @@ -65,7 +65,6 @@ export const findList = async ({ const { body: totalCount } = await esClient.count({ body: { - // @ts-expect-error GetQueryFilterReturn is not compatible with QueryDslQueryContainer query, }, ignore_unavailable: true, @@ -78,7 +77,6 @@ export const findList = async ({ // to explicitly define the type . const { body: response } = await esClient.search({ body: { - // @ts-expect-error GetQueryFilterReturn is not compatible with QueryDslQueryContainer query, search_after: scroll.searchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts index ff0e42870ab30..a467cbd9d60fe 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { DslQuery, EsQueryConfig } from '@kbn/es-query'; - -import { Filter, Query, esQuery } from '../../../../../../src/plugins/data/server'; +import { BoolQuery, EsQueryConfig, Query, buildEsQuery } from '@kbn/es-query'; import { escapeQuotes } from './escape_query'; @@ -21,7 +19,7 @@ export interface GetQueryFilterWithListIdOptions { } export interface GetQueryFilterReturn { - bool: { must: DslQuery[]; filter: Filter[]; should: never[]; must_not: Filter[] }; + bool: BoolQuery; } export const getQueryFilter = ({ filter }: GetQueryFilterOptions): GetQueryFilterReturn => { @@ -36,7 +34,7 @@ export const getQueryFilter = ({ filter }: GetQueryFilterOptions): GetQueryFilte queryStringOptions: { analyze_wildcard: true }, }; - return esQuery.buildEsQuery(undefined, kqlQuery, [], config); + return buildEsQuery(undefined, kqlQuery, [], config); }; export const getQueryFilterWithListId = ({ diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts index e34b3080dd33b..dbc83bb44e5fd 100644 --- a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts @@ -46,7 +46,6 @@ export const getSearchAfterScroll = async ({ const { body: response } = await esClient.search>({ body: { _source: getSourceWithTieBreaker({ sortField }), - // @ts-expect-error Filter is not assignale to QueryDslQueryContainer query, search_after: newSearchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), diff --git a/x-pack/plugins/ml/common/types/es_client.ts b/x-pack/plugins/ml/common/types/es_client.ts index b3d36283b5d5e..e9600a5465608 100644 --- a/x-pack/plugins/ml/common/types/es_client.ts +++ b/x-pack/plugins/ml/common/types/es_client.ts @@ -8,7 +8,7 @@ import { estypes } from '@elastic/elasticsearch'; import { JsonObject } from '@kbn/utility-types'; -import { buildEsQuery, DslQuery } from '@kbn/es-query'; +import { buildEsQuery } from '@kbn/es-query'; import { isPopulatedObject } from '../util/object_utils'; @@ -26,4 +26,7 @@ export const ES_CLIENT_TOTAL_HITS_RELATION: Record< GTE: 'gte', } as const; -export type InfluencersFilterQuery = ReturnType | DslQuery | JsonObject; +export type InfluencersFilterQuery = + | ReturnType + | estypes.QueryDslQueryContainer + | JsonObject; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx index 57e051e1b8417..45312011d7141 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx @@ -58,9 +58,9 @@ export function getKqlQueryValues({ influencersFilterQuery = esQuery.luceneStringToDsl(inputString); } - const clearSettings = - influencersFilterQuery?.match_all && Object.keys(influencersFilterQuery.match_all).length === 0; - + const clearSettings = Boolean( + influencersFilterQuery?.match_all && Object.keys(influencersFilterQuery.match_all).length === 0 + ); return { clearSettings, settings: { diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index 8a152ab1cadc3..a06db20210c1b 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -36,7 +36,7 @@ export interface ExplorerState { filteredFields: any[]; filterPlaceHolder: any; indexPattern: { title: string; fields: any[] }; - influencersFilterQuery: InfluencersFilterQuery; + influencersFilterQuery?: InfluencersFilterQuery; influencers: Dictionary; isAndOperator: boolean; loading: boolean; diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index f844eb0d5d601..3111c7f134da0 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { SerializableRecord } from '@kbn/utility-types'; import { createAction } from '../../../../../src/plugins/ui_actions/public'; import { MlCoreSetup } from '../plugin'; import { ML_APP_LOCATOR } from '../../common/constants/locator'; @@ -103,7 +104,7 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta pageState: { jobIds, timeRange, - ...(mlExplorerFilter ? { mlExplorerFilter } : {}), + ...(mlExplorerFilter ? ({ mlExplorerFilter } as SerializableRecord) : {}), query: {}, }, }); diff --git a/x-pack/plugins/osquery/common/typed_json.ts b/x-pack/plugins/osquery/common/typed_json.ts index 3735778b87491..527a8ed381654 100644 --- a/x-pack/plugins/osquery/common/typed_json.ts +++ b/x-pack/plugins/osquery/common/typed_json.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DslQuery, Filter } from '@kbn/es-query'; +import { BoolQuery } from '@kbn/es-query'; import { JsonObject } from '@kbn/utility-types'; export type ESQuery = @@ -48,10 +48,5 @@ export interface ESTermQuery { } export interface ESBoolQuery { - bool: { - must: DslQuery[]; - filter: Filter[]; - should: never[]; - must_not: Filter[]; - }; + bool: BoolQuery; } diff --git a/x-pack/plugins/security_solution/common/typed_json.ts b/x-pack/plugins/security_solution/common/typed_json.ts index c1d281eccb1fa..527a8ed381654 100644 --- a/x-pack/plugins/security_solution/common/typed_json.ts +++ b/x-pack/plugins/security_solution/common/typed_json.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { DslQuery, Filter } from '@kbn/es-query'; - +import { BoolQuery } from '@kbn/es-query'; import { JsonObject } from '@kbn/utility-types'; export type ESQuery = @@ -49,10 +48,5 @@ export interface ESTermQuery { } export interface ESBoolQuery { - bool: { - must: DslQuery[]; - filter: Filter[]; - should: never[]; - must_not: Filter[]; - }; + bool: BoolQuery; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts index 8fab8f30fb3dc..2e10f467b9fc8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts @@ -54,7 +54,6 @@ export const getThreatList = async ({ ); const { body: response } = await esClient.search({ body: { - // @ts-expect-error ESBoolQuery is not assignale to QueryDslQueryContainer query: queryFilter, fields: [ { @@ -126,7 +125,6 @@ export const getThreatListCount = async ({ ); const { body: response } = await esClient.count({ body: { - // @ts-expect-error ESBoolQuery is not assignale to QueryDslQueryContainer query: queryFilter, }, ignore_unavailable: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 89233cf2c8242..3dfe3fb650ecb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { DslQuery, Filter } from '@kbn/es-query'; +import { BoolQuery } from '@kbn/es-query'; import moment from 'moment'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -274,12 +274,7 @@ export type BulkResponseErrorAggregation = Record Promise; diff --git a/x-pack/plugins/timelines/common/typed_json.ts b/x-pack/plugins/timelines/common/typed_json.ts index 679a68a16f700..8f836b95a1711 100644 --- a/x-pack/plugins/timelines/common/typed_json.ts +++ b/x-pack/plugins/timelines/common/typed_json.ts @@ -5,8 +5,7 @@ * 2.0. */ import { JsonObject } from '@kbn/utility-types'; - -import { DslQuery, Filter } from '@kbn/es-query'; +import { BoolQuery } from '@kbn/es-query'; export type ESQuery = | ESRangeQuery @@ -48,10 +47,5 @@ export interface ESTermQuery { } export interface ESBoolQuery { - bool: { - must: DslQuery[]; - filter: Filter[]; - should: never[]; - must_not: Filter[]; - }; + bool: BoolQuery; } From 4291a1507bd794c763a02af492656130d6bdeb78 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Thu, 12 Aug 2021 15:54:39 -0400 Subject: [PATCH 22/91] [Presentation] Adds owner to presentation team plugin kibana.json. Updated CODEOWNERS (#108408) * Adds owner to presentation team plugin kibana.json. Updated CODEOWNERS * Adds a few more owners for presentation --- .github/CODEOWNERS | 6 ++ src/plugins/dashboard/kibana.json | 18 ++-- src/plugins/expression_error/kibana.json | 5 ++ src/plugins/expression_image/kibana.json | 5 ++ src/plugins/expression_metric/kibana.json | 5 ++ .../expression_repeat_image/kibana.json | 5 ++ .../expression_reveal_image/kibana.json | 5 ++ src/plugins/expression_shape/kibana.json | 9 +- src/plugins/input_control_vis/kibana.json | 16 ++-- src/plugins/presentation_util/kibana.json | 13 +-- src/plugins/vis_type_markdown/kibana.json | 25 +++--- x-pack/plugins/canvas/kibana.json | 83 ++++++++++--------- x-pack/plugins/dashboard_mode/kibana.json | 15 ++-- 13 files changed, 117 insertions(+), 93 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 26616ab237660..d98cde7b48c21 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -125,6 +125,12 @@ # Presentation /src/plugins/dashboard/ @elastic/kibana-presentation +/src/plugins/expression_error/ @elastic/kibana-presentation +/src/plugins/expression_image/ @elastic/kibana-presentation +/src/plugins/expression_metric/ @elastic/kibana-presentation +/src/plugins/expression_repeat_image/ @elastic/kibana-presentation +/src/plugins/expression_reveal_image/ @elastic/kibana-presentation +/src/plugins/expression_shape/ @elastic/kibana-presentation /src/plugins/input_control_vis/ @elastic/kibana-presentation /src/plugins/vis_type_markdown/ @elastic/kibana-presentation /src/plugins/presentation_util/ @elastic/kibana-presentation diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 54eaf461b73d7..d270b7dad3c7c 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -1,5 +1,10 @@ { "id": "dashboard", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds the Dashboard app to Kibana", "version": "kibana", "requiredPlugins": [ "data", @@ -14,17 +19,8 @@ "presentationUtil", "visualizations" ], - "optionalPlugins": [ - "home", - "spacesOss", - "savedObjectsTaggingOss", - "usageCollection"], + "optionalPlugins": ["home", "spacesOss", "savedObjectsTaggingOss", "usageCollection"], "server": true, "ui": true, - "requiredBundles": [ - "home", - "kibanaReact", - "kibanaUtils", - "presentationUtil" - ] + "requiredBundles": ["home", "kibanaReact", "kibanaUtils", "presentationUtil"] } diff --git a/src/plugins/expression_error/kibana.json b/src/plugins/expression_error/kibana.json index 9d8dd566d5b3a..aa3201694619c 100755 --- a/src/plugins/expression_error/kibana.json +++ b/src/plugins/expression_error/kibana.json @@ -1,5 +1,10 @@ { "id": "expressionError", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds 'error' renderer to expressions", "version": "1.0.0", "kibanaVersion": "kibana", "server": false, diff --git a/src/plugins/expression_image/kibana.json b/src/plugins/expression_image/kibana.json index 13b4e989b8f70..4f4b736d82d1a 100755 --- a/src/plugins/expression_image/kibana.json +++ b/src/plugins/expression_image/kibana.json @@ -1,5 +1,10 @@ { "id": "expressionImage", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds 'image' function and renderer to expressions", "version": "1.0.0", "kibanaVersion": "kibana", "server": true, diff --git a/src/plugins/expression_metric/kibana.json b/src/plugins/expression_metric/kibana.json index c83a3fcb26687..2aaef04e3bec3 100755 --- a/src/plugins/expression_metric/kibana.json +++ b/src/plugins/expression_metric/kibana.json @@ -1,5 +1,10 @@ { "id": "expressionMetric", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds 'metric' function and renderer to expressions", "version": "1.0.0", "kibanaVersion": "kibana", "server": true, diff --git a/src/plugins/expression_repeat_image/kibana.json b/src/plugins/expression_repeat_image/kibana.json index 33f1f9c8b759d..5694e0160042c 100755 --- a/src/plugins/expression_repeat_image/kibana.json +++ b/src/plugins/expression_repeat_image/kibana.json @@ -1,5 +1,10 @@ { "id": "expressionRepeatImage", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds 'repeatImage' function and renderer to expressions", "version": "1.0.0", "kibanaVersion": "kibana", "server": true, diff --git a/src/plugins/expression_reveal_image/kibana.json b/src/plugins/expression_reveal_image/kibana.json index 9af9a5857dcfb..dad7fdfe2bc5f 100755 --- a/src/plugins/expression_reveal_image/kibana.json +++ b/src/plugins/expression_reveal_image/kibana.json @@ -1,5 +1,10 @@ { "id": "expressionRevealImage", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds 'revealImage' function and renderer to expressions", "version": "1.0.0", "kibanaVersion": "kibana", "server": true, diff --git a/src/plugins/expression_shape/kibana.json b/src/plugins/expression_shape/kibana.json index 1a868288a2df8..adf95689e271b 100755 --- a/src/plugins/expression_shape/kibana.json +++ b/src/plugins/expression_shape/kibana.json @@ -1,12 +1,15 @@ { "id": "expressionShape", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds 'shape' function and renderer to expressions", "version": "1.0.0", "kibanaVersion": "kibana", "server": true, "ui": true, - "extraPublicDirs": [ - "common" - ], + "extraPublicDirs": ["common"], "requiredPlugins": ["expressions", "presentationUtil"], "optionalPlugins": [], "requiredBundles": [] diff --git a/src/plugins/input_control_vis/kibana.json b/src/plugins/input_control_vis/kibana.json index d05c5072c22e3..83d2c92a70093 100644 --- a/src/plugins/input_control_vis/kibana.json +++ b/src/plugins/input_control_vis/kibana.json @@ -1,16 +1,14 @@ { "id": "inputControlVis", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds Input Control visualization to Kibana", "version": "8.0.0", "kibanaVersion": "kibana", "server": false, "ui": true, - "requiredPlugins": [ - "data", - "expressions", - "visDefaultEditor", - "visualizations" - ], - "requiredBundles": [ - "kibanaReact" - ] + "requiredPlugins": ["data", "expressions", "visDefaultEditor", "visualizations"], + "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index 22ec919457cce..d7fe9b558e606 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -1,14 +1,15 @@ { "id": "presentationUtil", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas).", "version": "1.0.0", "kibanaVersion": "kibana", "server": true, "ui": true, - "extraPublicDirs": [ - "common/lib" - ], - "requiredPlugins": [ - "savedObjects" - ], + "extraPublicDirs": ["common/lib"], + "requiredPlugins": ["savedObjects"], "optionalPlugins": [] } diff --git a/src/plugins/vis_type_markdown/kibana.json b/src/plugins/vis_type_markdown/kibana.json index e14b4fe7bce96..49744ed1f7435 100644 --- a/src/plugins/vis_type_markdown/kibana.json +++ b/src/plugins/vis_type_markdown/kibana.json @@ -1,16 +1,13 @@ { - "id": "visTypeMarkdown", - "version": "kibana", - "ui": true, - "server": true, - "requiredPlugins": [ - "expressions", - "visualizations" - ], - "requiredBundles": [ - "expressions", - "kibanaReact", - "visDefaultEditor", - "visualizations" - ] + "id": "visTypeMarkdown", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds a markdown visualization type", + "version": "kibana", + "ui": true, + "server": true, + "requiredPlugins": ["expressions", "visualizations"], + "requiredBundles": ["expressions", "kibanaReact", "visDefaultEditor", "visualizations"] } diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index c465176e7ed01..201fb5ab8f78f 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -1,42 +1,43 @@ { - "id": "canvas", - "version": "8.0.0", - "kibanaVersion": "kibana", - "configPath": ["xpack", "canvas"], - "server": true, - "ui": true, - "requiredPlugins": [ - "bfetch", - "charts", - "data", - "embeddable", - "expressionError", - "expressionImage", - "expressionMetric", - "expressionRepeatImage", - "expressionRevealImage", - "expressionShape", - "expressions", - "features", - "inspector", - "presentationUtil", - "uiActions" - ], - "optionalPlugins": [ - "home", - "reporting", - "usageCollection" - ], - "requiredBundles": [ - "discover", - "home", - "kibanaLegacy", - "kibanaReact", - "kibanaUtils", - "lens", - "maps", - "savedObjects", - "visualizations", - "fieldFormats" - ] - } + "id": "canvas", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds Canvas application to Kibana", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "canvas"], + "server": true, + "ui": true, + "requiredPlugins": [ + "bfetch", + "charts", + "data", + "embeddable", + "expressionError", + "expressionImage", + "expressionMetric", + "expressionRepeatImage", + "expressionRevealImage", + "expressionShape", + "expressions", + "features", + "inspector", + "presentationUtil", + "uiActions" + ], + "optionalPlugins": ["home", "reporting", "usageCollection"], + "requiredBundles": [ + "discover", + "home", + "kibanaLegacy", + "kibanaReact", + "kibanaUtils", + "lens", + "maps", + "savedObjects", + "visualizations", + "fieldFormats" + ] +} diff --git a/x-pack/plugins/dashboard_mode/kibana.json b/x-pack/plugins/dashboard_mode/kibana.json index 81e2073b5c7fd..6fc59ce9a7fa1 100644 --- a/x-pack/plugins/dashboard_mode/kibana.json +++ b/x-pack/plugins/dashboard_mode/kibana.json @@ -1,17 +1,14 @@ { "id": "dashboardMode", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "dashboard_mode" - ], + "configPath": ["xpack", "dashboard_mode"], "optionalPlugins": ["security"], - "requiredPlugins": [ - "kibanaLegacy", - "urlForwarding", - "dashboard" - ], + "requiredPlugins": ["kibanaLegacy", "urlForwarding", "dashboard"], "server": true, "ui": true } From 45b33ba806dcfb3801088255df8ea75cf9363bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 12 Aug 2021 22:22:21 +0200 Subject: [PATCH 23/91] [Observability] Remove outdated top_alerts route and related types (#107579) * Remove outdated top_alerts route and related types * Remove tests for deleted code * Remove test for deleted API * Remove reference to deleted type * Remove unused translations * Remove unused mock from story * Remove no-op alerts page story for now * Remove unsafe type assertions * Factor out alert field type * Compile kbn-io-ts-utils for the browser as well * Avoid deep import which doesn't work cross-platform * Revert "Avoid deep import which doesn't work cross-platform" This reverts commit 492378c6b59c44da081e71857828a54389a9c554. * Revert "Compile kbn-io-ts-utils for the browser as well" This reverts commit a1267b139d10f7ea209fe51ebb9967fd04cd9594. * Revert "Factor out alert field type" This reverts commit def69874987fb82bf4422ca8b1f045654bb65b12. * Revert "Remove unsafe type assertions" This reverts commit c88d4cd005df9e50e4ad485b4e6cb6880b784954. * Remove unsafe type assertions (again) --- .../public/pages/alerts/alerts.stories.tsx | 98 ----------- .../alerts_flyout/alerts_flyout.stories.tsx | 3 +- .../pages/alerts/alerts_flyout/index.tsx | 9 +- .../public/pages/alerts/alerts_table.tsx | 163 ------------------ .../pages/alerts/alerts_table_t_grid.tsx | 20 +-- .../public/pages/alerts/index.tsx | 3 - .../{decorate_response.ts => parse_alert.ts} | 39 ++--- .../public/pages/alerts/render_cell_value.tsx | 8 +- .../server/lib/rules/get_top_alerts.ts | 52 ------ .../observability/server/routes/rules.ts | 45 +---- .../server/utils/queries.test.ts | 39 ----- .../observability/server/utils/queries.ts | 10 -- .../translations/translations/ja-JP.json | 8 - .../translations/translations/zh-CN.json | 8 - .../tests/alerts/rule_registry.ts | 99 ----------- 15 files changed, 38 insertions(+), 566 deletions(-) delete mode 100644 x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx delete mode 100644 x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx rename x-pack/plugins/observability/public/pages/alerts/{decorate_response.ts => parse_alert.ts} (63%) delete mode 100644 x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts delete mode 100644 x-pack/plugins/observability/server/utils/queries.test.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx deleted file mode 100644 index cf5d8ad2817fe..0000000000000 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { StoryContext } from '@storybook/react'; -import React, { ComponentType } from 'react'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; -import { MemoryRouter } from 'react-router-dom'; -import { AlertsPage } from '.'; -import { HttpSetup } from '../../../../../../src/core/public'; -import { - KibanaContextProvider, - KibanaPageTemplate, -} from '../../../../../../src/plugins/kibana_react/public'; -import { PluginContext, PluginContextValue } from '../../context/plugin_context'; -import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; -import { createCallObservabilityApi } from '../../services/call_observability_api'; -import type { ObservabilityAPIReturnType } from '../../services/call_observability_api/types'; -import { apmAlertResponseExample, dynamicIndexPattern } from './example_data'; - -interface PageArgs { - items: ObservabilityAPIReturnType<'GET /api/observability/rules/alerts/top'>; -} - -export default { - title: 'app/Alerts', - component: AlertsPage, - decorators: [ - (Story: ComponentType, { args: { items = [] } }: StoryContext) => { - createCallObservabilityApi(({ - get: async (endpoint: string) => { - if (endpoint === '/api/observability/rules/alerts/top') { - return items; - } else if (endpoint === '/api/observability/rules/alerts/dynamic_index_pattern') { - return dynamicIndexPattern; - } - }, - } as unknown) as HttpSetup); - - return ( - - - '' }, - data: { autocomplete: { hasQuerySuggestions: () => false }, query: {} }, - chrome: { docTitle: { change: () => {} } }, - docLinks: { links: { query: {} } }, - storage: { get: () => {} }, - timelines: { getTGrid: () => <> }, - uiSettings: { - get: (setting: string) => { - if (setting === 'dateFormat') { - return ''; - } else { - return []; - } - }, - }, - }} - > - '' } }, - }, - observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), - ObservabilityPageTemplate: KibanaPageTemplate, - } as unknown) as PluginContextValue - } - > - - - - - - ); - }, - ], -}; - -export function Example(_args: PageArgs) { - return ( - - ); -} -Example.args = { - items: apmAlertResponseExample, -} as PageArgs; - -export function EmptyState(_args: PageArgs) { - return ; -} -EmptyState.args = { items: [] } as PageArgs; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx index a752b8d89a1bd..f507802a1e7d1 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx @@ -7,7 +7,6 @@ import { ALERT_UUID } from '@kbn/rule-data-utils'; import React, { ComponentType } from 'react'; -import type { TopAlertResponse } from '../'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { PluginContext, PluginContextValue } from '../../../context/plugin_context'; import { createObservabilityRuleTypeRegistryMock } from '../../../rules/observability_rule_type_registry_mock'; @@ -15,7 +14,7 @@ import { apmAlertResponseExample } from '../example_data'; import { AlertsFlyout } from './'; interface Args { - alerts: TopAlertResponse[]; + alerts: Array>; } export default { diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 3d63f7bdaeaf7..90032419948ef 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -41,16 +41,16 @@ import { } from '@kbn/rule-data-utils/target_node/technical_field_names'; import moment from 'moment-timezone'; import React, { useMemo } from 'react'; -import type { TopAlert, TopAlertResponse } from '../'; +import type { TopAlert } from '../'; import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; import { asDuration } from '../../../../common/utils/formatters'; import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; -import { decorateResponse } from '../decorate_response'; +import { parseAlert } from '../parse_alert'; import { SeverityBadge } from '../severity_badge'; type AlertsFlyoutProps = { alert?: TopAlert; - alerts?: TopAlertResponse[]; + alerts?: Array>; isInApp?: boolean; observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; selectedAlertId?: string; @@ -77,7 +77,8 @@ export function AlertsFlyout({ const { http } = services; const prepend = http?.basePath.prepend; const decoratedAlerts = useMemo(() => { - return decorateResponse(alerts ?? [], observabilityRuleTypeRegistry); + const parseObservabilityAlert = parseAlert(observabilityRuleTypeRegistry); + return (alerts ?? []).map(parseObservabilityAlert); }, [alerts, observabilityRuleTypeRegistry]); let alertData = alert; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx deleted file mode 100644 index 395c2a5253ec6..0000000000000 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - CustomItemAction, - EuiBasicTable, - EuiBasicTableColumn, - EuiButton, - EuiIconTip, - EuiLink, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ALERT_DURATION, ALERT_SEVERITY_LEVEL, ALERT_UUID } from '@kbn/rule-data-utils'; -import React, { Suspense, useMemo, useState } from 'react'; -import { LazyAlertsFlyout } from '../..'; -import { asDuration } from '../../../common/utils/formatters'; -import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; -import { usePluginContext } from '../../hooks/use_plugin_context'; -import type { TopAlert, TopAlertResponse } from './'; -import { decorateResponse } from './decorate_response'; -import { SeverityBadge } from './severity_badge'; - -const pagination = { pageIndex: 0, pageSize: 0, totalItemCount: 0 }; - -interface AlertsTableProps { - items: TopAlertResponse[]; -} - -export function AlertsTable(props: AlertsTableProps) { - const [selectedAlertId, setSelectedAlertId] = useState(undefined); - const handleFlyoutClose = () => setSelectedAlertId(undefined); - const { core, observabilityRuleTypeRegistry } = usePluginContext(); - const { prepend } = core.http.basePath; - const items = useMemo(() => decorateResponse(props.items, observabilityRuleTypeRegistry), [ - props.items, - observabilityRuleTypeRegistry, - ]); - - const actions: Array> = useMemo( - () => [ - { - render: (alert) => - alert.link ? ( - - {i18n.translate('xpack.observability.alertsTable.viewInAppButtonLabel', { - defaultMessage: 'View in app', - })} - - ) : ( - <> - ), - isPrimary: true, - }, - ], - [prepend] - ); - - const columns: Array> = useMemo( - () => [ - { - field: 'active', - name: i18n.translate('xpack.observability.alertsTable.statusColumnDescription', { - defaultMessage: 'Status', - }), - align: 'center', - render: (_, alert) => { - const { active } = alert; - - return active ? ( - - ) : ( - - ); - }, - }, - { - field: 'start', - name: i18n.translate('xpack.observability.alertsTable.triggeredColumnDescription', { - defaultMessage: 'Triggered', - }), - render: (_, alert) => { - return ( - - ); - }, - }, - { - field: 'duration', - name: i18n.translate('xpack.observability.alertsTable.durationColumnDescription', { - defaultMessage: 'Duration', - }), - render: (_, alert) => { - const { active } = alert; - return active ? null : asDuration(alert.fields[ALERT_DURATION], { extended: true }); - }, - }, - { - field: 'severity', - name: i18n.translate('xpack.observability.alertsTable.severityColumnDescription', { - defaultMessage: 'Severity', - }), - render: (_, alert) => { - return ; - }, - }, - { - field: 'reason', - name: i18n.translate('xpack.observability.alertsTable.reasonColumnDescription', { - defaultMessage: 'Reason', - }), - dataType: 'string', - render: (_, alert) => { - return ( - setSelectedAlertId(alert.fields[ALERT_UUID])}> - {alert.reason} - - ); - }, - }, - { - actions, - name: i18n.translate('xpack.observability.alertsTable.actionsColumnDescription', { - defaultMessage: 'Actions', - }), - }, - ], - [actions, setSelectedAlertId] - ); - - return ( - <> - - - - - columns={columns} - items={items} - tableLayout="auto" - pagination={pagination} - /> - - ); -} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 2f287ee1d614d..57c8225a01d26 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -32,8 +32,8 @@ import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/t import { EuiButtonIcon, EuiDataGridColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import React, { Suspense, useMemo, useState } from 'react'; -import React, { Suspense, useState } from 'react'; import type { TimelinesUIStart } from '../../../../timelines/public'; import type { TopAlert } from './'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; @@ -46,9 +46,9 @@ import type { import { getRenderCellValue } from './render_cell_value'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { decorateResponse } from './decorate_response'; import { getDefaultCellActions } from './default_cell_actions'; import { LazyAlertsFlyout } from '../..'; +import { parseAlert } from './parse_alert'; const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; @@ -152,6 +152,10 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { const handleFlyoutClose = () => setFlyoutAlert(undefined); const { timelines } = useKibana<{ timelines: TimelinesUIStart }>().services; + const parseObservabilityAlert = useMemo(() => parseAlert(observabilityRuleTypeRegistry), [ + observabilityRuleTypeRegistry, + ]); + const leadingControlColumns = [ { id: 'expand', @@ -167,11 +171,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { }, rowCellRender: ({ data }: ActionProps) => { const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); - const decoratedAlerts = decorateResponse( - [dataFieldEs] ?? [], - observabilityRuleTypeRegistry - ); - const alert = decoratedAlerts[0]; + const alert = parseObservabilityAlert(dataFieldEs); return ( null, rowCellRender: ({ data }: ActionProps) => { const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); - const decoratedAlerts = decorateResponse( - [dataFieldEs] ?? [], - observabilityRuleTypeRegistry - ); - const alert = decoratedAlerts[0]; + const alert = parseObservabilityAlert(dataFieldEs); return ( [number]; - export interface TopAlert { fields: ParsedTechnicalFields; start: number; diff --git a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts b/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts similarity index 63% rename from x-pack/plugins/observability/public/pages/alerts/decorate_response.ts rename to x-pack/plugins/observability/public/pages/alerts/parse_alert.ts index 7eb6d785779b7..4e99bdb0ee32d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts +++ b/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts @@ -18,7 +18,7 @@ import { ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; -import type { TopAlertResponse, TopAlert } from '.'; +import type { TopAlert } from '.'; import { parseTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; import { asDuration, asPercent } from '../../../common/utils/formatters'; import { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry'; @@ -28,24 +28,21 @@ const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; -export function decorateResponse( - alerts: TopAlertResponse[], - observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry -): TopAlert[] { - return alerts.map((alert) => { - const parsedFields = parseTechnicalFields(alert); - const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[ALERT_RULE_TYPE_ID]!); - const formatted = { - link: undefined, - reason: parsedFields[ALERT_RULE_NAME]!, - ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), - }; +export const parseAlert = (observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry) => ( + alert: Record +): TopAlert => { + const parsedFields = parseTechnicalFields(alert); + const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[ALERT_RULE_TYPE_ID]!); + const formatted = { + link: undefined, + reason: parsedFields[ALERT_RULE_NAME] ?? '', + ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), + }; - return { - ...formatted, - fields: parsedFields, - active: parsedFields[ALERT_STATUS] !== 'closed', - start: new Date(parsedFields[ALERT_START]!).getTime(), - }; - }); -} + return { + ...formatted, + fields: parsedFields, + active: parsedFields[ALERT_STATUS] !== 'closed', + start: new Date(parsedFields[ALERT_START] ?? 0).getTime(), + }; +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index c080b6b94ed4a..f6e1d41c2a6f9 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -32,7 +32,7 @@ import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; import { asDuration } from '../../../common/utils/formatters'; import { SeverityBadge } from './severity_badge'; import { TopAlert } from '.'; -import { decorateResponse } from './decorate_response'; +import { parseAlert } from './parse_alert'; import { usePluginContext } from '../../hooks/use_plugin_context'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; @@ -112,11 +112,7 @@ export const getRenderCellValue = ({ return ; case ALERT_RULE_NAME: const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); - const decoratedAlerts = decorateResponse( - [dataFieldEs] ?? [], - observabilityRuleTypeRegistry - ); - const alert = decoratedAlerts[0]; + const alert = parseAlert(observabilityRuleTypeRegistry)(dataFieldEs); return ( // NOTE: EuiLink automatically renders links using a
+`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/index.ts new file mode 100644 index 0000000000000..55661ad6f14f7 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './no_data_page'; +export * from './no_data_card'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap new file mode 100644 index 0000000000000..c8fda1d036439 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ElasticAgentCard props button 1`] = ` + + Button + + } + href="app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title="Add a Solution integration" +/> +`; + +exports[`ElasticAgentCard props href 1`] = ` + + Button + + } + href="#" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title="Add a Solution integration" +/> +`; + +exports[`ElasticAgentCard props recommended 1`] = ` + + Find an integration for Solution + + } + href="app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title="Add a Solution integration" +/> +`; + +exports[`ElasticAgentCard renders 1`] = ` + + Find an integration for Solution + + } + href="app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title="Add a Solution integration" +/> +`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap new file mode 100644 index 0000000000000..1146e4f676eb6 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ElasticBeatsCard props button 1`] = ` + + Button + + } + href="app/home#/tutorial" + image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" + paddingSize="l" + title="Add data with Beats" +/> +`; + +exports[`ElasticBeatsCard props href 1`] = ` + + Button + + } + href="#" + image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" + paddingSize="l" + title="Add data with Beats" +/> +`; + +exports[`ElasticBeatsCard props recommended 1`] = ` + + Install Beats for Solution + + } + href="app/home#/tutorial" + image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" + paddingSize="l" + title="Add data with Beats" +/> +`; + +exports[`ElasticBeatsCard renders 1`] = ` + + Install Beats for Solution + + } + href="app/home#/tutorial" + image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" + paddingSize="l" + title="Add data with Beats" +/> +`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap new file mode 100644 index 0000000000000..a8232c209ed73 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NoDataCard props button 1`] = ` +
+
+ + Card title + +
+

+ Description +

+
+
+ +
+`; + +exports[`NoDataCard props href 1`] = ` +
+`; + +exports[`NoDataCard props recommended 1`] = ` +
+
+ + Card title + +
+

+ Description +

+
+
+ + + Recommended + + +
+`; + +exports[`NoDataCard renders 1`] = ` +
+
+ + Card title + +
+

+ Description +

+
+
+
+`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx new file mode 100644 index 0000000000000..45cc32cae06d6 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { ElasticAgentCard } from './elastic_agent_card'; + +jest.mock('../../../context', () => ({ + ...jest.requireActual('../../../context'), + useKibana: jest.fn().mockReturnValue({ + services: { + http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, + uiSettings: { get: jest.fn() }, + }, + }), +})); + +describe('ElasticAgentCard', () => { + test('renders', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + test('recommended', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('button', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('href', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx new file mode 100644 index 0000000000000..f0ee2fc2739d9 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -0,0 +1,69 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @elastic/eui/href-or-on-click */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; +import { EuiButton, EuiCard } from '@elastic/eui'; +import { useKibana } from '../../../context'; +import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; + +export type ElasticAgentCardProps = NoDataPageActions & { + solution: string; +}; + +/** + * Applies extra styling to a typical EuiAvatar + */ +export const ElasticAgentCard: FunctionComponent = ({ + solution, + recommended = true, + href = 'app/integrations/browse', + button, + ...cardRest +}) => { + const { + services: { http }, + } = useKibana(); + const addBasePath = http.basePath.prepend; + const basePathUrl = '/plugins/kibanaReact/assets/'; + + const footer = + typeof button !== 'string' && typeof button !== 'undefined' ? ( + button + ) : ( + + {button || + i18n.translate('kibana-react.noDataPage.elasticAgentCard.buttonLabel', { + defaultMessage: 'Find an integration for {solution}', + values: { solution }, + })} + + ); + + return ( + + ); +}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx new file mode 100644 index 0000000000000..6ea41bf6b3e1f --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { ElasticBeatsCard } from './elastic_beats_card'; + +jest.mock('../../../context', () => ({ + ...jest.requireActual('../../../context'), + useKibana: jest.fn().mockReturnValue({ + services: { + http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, + uiSettings: { get: jest.fn() }, + }, + }), +})); + +describe('ElasticBeatsCard', () => { + test('renders', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + test('recommended', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('button', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('href', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx new file mode 100644 index 0000000000000..cf147315c97f4 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx @@ -0,0 +1,68 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @elastic/eui/href-or-on-click */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; +import { EuiButton, EuiCard } from '@elastic/eui'; +import { useKibana } from '../../../context'; +import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; + +export type ElasticBeatsCardProps = NoDataPageActions & { + solution: string; +}; + +export const ElasticBeatsCard: FunctionComponent = ({ + recommended, + href = 'app/home#/tutorial', + button, + solution, + ...cardRest +}) => { + const { + services: { http, uiSettings }, + } = useKibana(); + const addBasePath = http.basePath.prepend; + const basePathUrl = '/plugins/kibanaReact/assets/'; + const IS_DARK_THEME = uiSettings.get('theme:darkMode'); + + const footer = + typeof button !== 'string' && typeof button !== 'undefined' ? ( + button + ) : ( + + {button || + i18n.translate('kibana-react.noDataPage.elasticBeatsCard.buttonLabel', { + defaultMessage: 'Install Beats for {solution}', + values: { solution }, + })} + + ); + + return ( + + ); +}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts new file mode 100644 index 0000000000000..3744239d9a472 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts @@ -0,0 +1,11 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './elastic_agent_card'; +export * from './elastic_beats_card'; +export * from './no_data_card'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.test.tsx new file mode 100644 index 0000000000000..a809ede2dc617 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.test.tsx @@ -0,0 +1,41 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { render } from 'enzyme'; +import React from 'react'; +import { NoDataCard } from './no_data_card'; + +describe('NoDataCard', () => { + test('renders', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + test('recommended', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + + test('button', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + + test('href', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx new file mode 100644 index 0000000000000..0be85f8c8ed1c --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx @@ -0,0 +1,40 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @elastic/eui/href-or-on-click */ + +import React, { FunctionComponent } from 'react'; +import { EuiButton, EuiCard, EuiCardProps } from '@elastic/eui'; +import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; + +// Custom cards require all the props the EuiCard does +type NoDataCard = EuiCardProps & NoDataPageActions; + +export const NoDataCard: FunctionComponent = ({ + recommended, + button, + ...cardRest +}) => { + const footer = + typeof button !== 'string' ? ( + button + ) : ( + + {button} + + ); + + return ( + + ); +}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.scss b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.scss new file mode 100644 index 0000000000000..f1bc12e74cf4e --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.scss @@ -0,0 +1,7 @@ +.kbnNoDataPageContents__item:only-child { + min-width: 400px; + + @include euiBreakpoint('xs', 's') { + min-width: auto; + } +} diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.test.tsx new file mode 100644 index 0000000000000..59d6e8280af98 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { NoDataPage } from './no_data_page'; +import { shallowWithIntl } from '@kbn/test/jest'; + +describe('NoDataPage', () => { + test('render', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx new file mode 100644 index 0000000000000..56eb0f34617d6 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx @@ -0,0 +1,203 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './no_data_page.scss'; + +import React, { ReactNode, useMemo, FunctionComponent, MouseEventHandler } from 'react'; +import { + EuiFlexItem, + EuiCardProps, + EuiFlexGrid, + EuiSpacer, + EuiText, + EuiTextColor, + EuiLink, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { KibanaPageTemplateProps } from '../page_template'; + +import { ElasticAgentCard, ElasticBeatsCard, NoDataCard } from './no_data_card'; +import { KibanaPageTemplateSolutionNavAvatar } from '../solution_nav'; + +export const NO_DATA_PAGE_MAX_WIDTH = 950; +export const NO_DATA_PAGE_TEMPLATE_PROPS: KibanaPageTemplateProps = { + restrictWidth: NO_DATA_PAGE_MAX_WIDTH, + template: 'centeredBody', + pageContentProps: { + hasShadow: false, + color: 'transparent', + }, +}; + +export const NO_DATA_RECOMMENDED = i18n.translate( + 'kibana-react.noDataPage.noDataPage.recommended', + { + defaultMessage: 'Recommended', + } +); + +export type NoDataPageActions = Partial & { + /** + * Applies the `Recommended` beta badge and makes the button `fill` + */ + recommended?: boolean; + /** + * Provide just a string for the button's label, or a whole component + */ + button?: string | ReactNode; + /** + * Remapping `onClick` to any element + */ + onClick?: MouseEventHandler; +}; + +export type NoDataPageActionsProps = Record; + +export interface NoDataPageProps { + /** + * Single name for the current solution, used to auto-generate the title, logo, description, and button label + */ + solution: string; + /** + * Optionally replace the auto-generated logo + */ + logo?: string; + /** + * Required to set the docs link for the whole solution + */ + docsLink: string; + /** + * Optionally replace the auto-generated page title (h1) + */ + pageTitle?: string; + /** + * An object of `NoDataPageActions` configurations with unique primary keys. + * Use `elasticAgent` or `beats` as the primary key for pre-configured cards of this type. + * Otherwise use a custom key that contains `EuiCard` props. + */ + actions: NoDataPageActionsProps; +} + +export const NoDataPage: FunctionComponent = ({ + solution, + logo, + actions, + docsLink, + pageTitle, +}) => { + // Convert obj data into an iterable array + const entries = Object.entries(actions); + + // This sort fn may look nonsensical, but it's some Good Ol' Javascript (TM) + // Sort functions want either a 1, 0, or -1 returned to determine order, + // and it turns out in JS you CAN minus booleans from each other to get a 1, 0, or -1 - e.g., (true - false == 1) :whoa: + const sortedEntries = entries.sort(([, firstObj], [, secondObj]) => { + // The `??` fallbacks are because the recommended key can be missing or undefined + return Number(secondObj.recommended ?? false) - Number(firstObj.recommended ?? false); + }); + + // Convert the iterated [[key, value]] array format back into an object + const sortedData = Object.fromEntries(sortedEntries); + const actionsKeys = Object.keys(sortedData); + const renderActions = useMemo(() => { + return Object.values(sortedData).map((action, i) => { + if (actionsKeys[i] === 'elasticAgent') { + return ( + + + + ); + } else if (actionsKeys[i] === 'beats') { + return ( + + + + ); + } else { + return ( + + + + ); + } + }); + }, [actions, sortedData, actionsKeys]); + + return ( +
+ + + +

+ {pageTitle || ( + + )} +

+ +

+ + + + ), + }} + /> +

+
+
+ + + + {renderActions} + + {actionsKeys.length > 1 ? ( + <> + + +

+ + + + ), + }} + /> +

+
+ + ) : undefined} +
+ ); +}; diff --git a/src/plugins/kibana_react/public/page_template/page_template.scss b/src/plugins/kibana_react/public/page_template/page_template.scss index 631511cd0475f..6b1c17e870e8f 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.scss +++ b/src/plugins/kibana_react/public/page_template/page_template.scss @@ -10,4 +10,8 @@ &.kbnPageTemplate__pageSideBar--shrink { min-width: $euiSizeXXL; } + + .kbnPageTemplate--centeredBody & { + border-right: $euiBorderThin; + } } diff --git a/src/plugins/kibana_react/public/page_template/page_template.test.tsx b/src/plugins/kibana_react/public/page_template/page_template.test.tsx index 2fdedce23b09b..6c6c4bb33e6bb 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.test.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { KibanaPageTemplate } from './page_template'; +import { KibanaPageTemplate, KibanaPageTemplateProps } from './page_template'; import { EuiEmptyPrompt } from '@elastic/eui'; import { KibanaPageTemplateSolutionNavProps } from './solution_nav'; @@ -51,6 +51,16 @@ const navItems: KibanaPageTemplateSolutionNavProps['items'] = [ }, ]; +const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = { + solution: 'Elastic', + actions: { + elasticAgent: {}, + beats: {}, + custom: {}, + }, + docsLink: 'test', +}; + describe('KibanaPageTemplate', () => { test('render default empty prompt', () => { const component = shallow( @@ -126,6 +136,26 @@ describe('KibanaPageTemplate', () => { expect(component).toMatchSnapshot(); }); + test('render noDataContent', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + test('render sidebar classes', () => { const component = shallow( = ({ template, + className, pageHeader, children, isEmptyState, @@ -50,6 +58,7 @@ export const KibanaPageTemplate: FunctionComponent = ({ pageSideBar, pageSideBarProps, solutionNav, + noDataConfig, ...rest }) => { /** @@ -86,11 +95,10 @@ export const KibanaPageTemplate: FunctionComponent = ({ ); } - const emptyStateDefaultTemplate = pageSideBar ? 'centeredContent' : 'centeredBody'; - /** * An easy way to create the right content for empty pages */ + const emptyStateDefaultTemplate = pageSideBar ? 'centeredContent' : 'centeredBody'; if (isEmptyState && pageHeader && !children) { template = template ?? emptyStateDefaultTemplate; const { iconType, pageTitle, description, rightSideItems } = pageHeader; @@ -110,9 +118,40 @@ export const KibanaPageTemplate: FunctionComponent = ({ template = template ?? emptyStateDefaultTemplate; } + // Set the template before the classes + template = noDataConfig ? NO_DATA_PAGE_TEMPLATE_PROPS.template : template; + + const classes = classNames( + 'kbnPageTemplate', + { [`kbnPageTemplate--${template}`]: template }, + className + ); + + /** + * If passing the custom template of `noDataConfig` + */ + if (noDataConfig) { + return ( + + + + ); + } + return ( & { + /** + * Any EuiAvatar size available, of `xxl` for custom large, brand-focused version + */ + size?: EuiAvatarProps['size'] | 'xxl'; +}; /** * Applies extra styling to a typical EuiAvatar */ export const KibanaPageTemplateSolutionNavAvatar: FunctionComponent = ({ className, + size, ...rest }) => { return ( + // @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine ); diff --git a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot index 02f54723abd42..8359b186e4afd 100644 --- a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot @@ -2,7 +2,7 @@ exports[`Storyshots Home Home Page 1`] = `
Date: Fri, 13 Aug 2021 04:04:43 +0400 Subject: [PATCH 31/91] [APM] Update throughput chart tooltip content (#108351) --- .../service_overview/service_overview_throughput_chart.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 741491f87f78c..a557011a0a3e0 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -142,11 +142,11 @@ export function ServiceOverviewThroughputChart({ data.throughputUnit === 'minute' ? i18n.translate('xpack.apm.serviceOverview.tpmHelp', { defaultMessage: - 'Throughput is measured in tpm (transactions per minute)', + 'Throughput is measured in transactions per minute (tpm)', }) : i18n.translate('xpack.apm.serviceOverview.tpsHelp', { defaultMessage: - 'Throughput is measured in tps (transactions per second)', + 'Throughput is measured in transactions per second (tps)', }) } position="right" From e235a0a8b0e8930a3adff9392d89c3093de45550 Mon Sep 17 00:00:00 2001 From: liza-mae Date: Thu, 12 Aug 2021 18:39:14 -0600 Subject: [PATCH 32/91] Fix unhandled promise rejection (#108430) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/functional/apps/lens/formula.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index 7662b32b8aee6..20ab5e165fda0 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -80,14 +80,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.sleep(100); - PageObjects.lens.expectFormulaText(`count(kql='Men\\'s Clothing ')`); + await PageObjects.lens.expectFormulaText(`count(kql='Men\\'s Clothing ')`); await PageObjects.lens.typeFormula('count(kql='); input = await find.activeElement(); await input.type(`Men\'s Clothing`); - PageObjects.lens.expectFormulaText(`count(kql='Men\\'s Clothing')`); + await PageObjects.lens.expectFormulaText(`count(kql='Men\\'s Clothing')`); }); it('should insert single quotes and escape when needed to create valid field name', async () => { @@ -109,7 +109,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.switchToFormula(); - PageObjects.lens.expectFormulaText(`unique_count('*\\' "\\'')`); + await PageObjects.lens.expectFormulaText(`unique_count('*\\' "\\'')`); await PageObjects.lens.typeFormula('unique_count('); const input = await find.activeElement(); @@ -118,7 +118,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.sleep(100); - PageObjects.lens.expectFormulaText(`unique_count('*\\' "\\'')`); + await PageObjects.lens.expectFormulaText(`unique_count('*\\' "\\'')`); }); it('should persist a broken formula on close', async () => { From b5bd063f51b633223db0f957a670b9c0af488631 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 12 Aug 2021 21:34:05 -0400 Subject: [PATCH 33/91] Deprecate ability to disable alerting, actions, task manager, stack alerts, and event log plugins (#108281) * Add deprecation warnings for .enabled config for all our plugins * Add tests * Add stackAlerts * Fix stack alerts * Add tests * Add triggers_action_ui * Add deprecated warning to the docs --- docs/settings/alert-action-settings.asciidoc | 2 +- x-pack/plugins/actions/server/index.test.ts | 42 +++++++++++++++++++ x-pack/plugins/actions/server/index.ts | 22 ++++++++-- x-pack/plugins/alerting/server/index.test.ts | 42 +++++++++++++++++++ x-pack/plugins/alerting/server/index.ts | 13 +++++- x-pack/plugins/event_log/server/index.ts | 23 ++++++++-- .../plugins/stack_alerts/server/index.test.ts | 42 +++++++++++++++++++ x-pack/plugins/stack_alerts/server/index.ts | 15 ++++++- .../plugins/task_manager/server/index.test.ts | 9 ++++ x-pack/plugins/task_manager/server/index.ts | 11 +++++ .../triggers_actions_ui/server/index.test.ts | 42 +++++++++++++++++++ .../triggers_actions_ui/server/index.ts | 15 ++++++- 12 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/actions/server/index.test.ts create mode 100644 x-pack/plugins/alerting/server/index.test.ts create mode 100644 x-pack/plugins/stack_alerts/server/index.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/server/index.test.ts diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index c9847effd5f49..f168195e10ef5 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -41,7 +41,7 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== | `xpack.actions.enabled` - | Feature toggle that enables Actions in {kib}. + | Deprecated. This will be removed in 8.0. Feature toggle that enables Actions in {kib}. If `false`, all features dependent on Actions are disabled, including the *Observability* and *Security* apps. Default: `true`. | `xpack.actions.allowedHosts` {ess-icon} diff --git a/x-pack/plugins/actions/server/index.test.ts b/x-pack/plugins/actions/server/index.test.ts new file mode 100644 index 0000000000000..56f3533b47e3f --- /dev/null +++ b/x-pack/plugins/actions/server/index.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { config } from './index'; +import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; + +const CONFIG_PATH = 'xpack.actions'; +const applyStackAlertDeprecations = (settings: Record = {}) => { + const deprecations = config.deprecations!(configDeprecationFactory); + const deprecationMessages: string[] = []; + const _config = { + [CONFIG_PATH]: settings, + }; + const { config: migrated } = applyDeprecations( + _config, + deprecations.map((deprecation) => ({ + deprecation, + path: CONFIG_PATH, + })), + () => ({ message }) => deprecationMessages.push(message) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('index', () => { + describe('deprecations', () => { + it('should deprecate .enabled flag', () => { + const { messages } = applyStackAlertDeprecations({ enabled: false }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.actions.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 230ed826cb108..bf59a1a11687d 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { get } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { ActionsPlugin } from './plugin'; @@ -59,7 +59,8 @@ export const config: PluginConfigDescriptor = { deprecations: ({ renameFromRoot, unused }) => [ renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts'), (settings, fromPath, addDeprecation) => { - const customHostSettings = settings?.xpack?.actions?.customHostSettings ?? []; + const actions = get(settings, fromPath); + const customHostSettings = actions?.customHostSettings ?? []; if ( customHostSettings.find( (customHostSchema: CustomHostSettings) => @@ -84,7 +85,8 @@ export const config: PluginConfigDescriptor = { } }, (settings, fromPath, addDeprecation) => { - if (!!settings?.xpack?.actions?.rejectUnauthorized) { + const actions = get(settings, fromPath); + if (!!actions?.rejectUnauthorized) { addDeprecation({ message: `"xpack.actions.rejectUnauthorized" is deprecated. Use "xpack.actions.verificationMode" instead, ` + @@ -102,7 +104,8 @@ export const config: PluginConfigDescriptor = { } }, (settings, fromPath, addDeprecation) => { - if (!!settings?.xpack?.actions?.proxyRejectUnauthorizedCertificates) { + const actions = get(settings, fromPath); + if (!!actions?.proxyRejectUnauthorizedCertificates) { addDeprecation({ message: `"xpack.actions.proxyRejectUnauthorizedCertificates" is deprecated. Use "xpack.actions.proxyVerificationMode" instead, ` + @@ -119,5 +122,16 @@ export const config: PluginConfigDescriptor = { }); } }, + (settings, fromPath, addDeprecation) => { + const actions = get(settings, fromPath); + if (actions?.enabled === false || actions?.enabled === true) { + addDeprecation({ + message: `"xpack.actions.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, + correctiveActions: { + manualSteps: [`Remove "xpack.actions.enabled" from your kibana configs.`], + }, + }); + } + }, ], }; diff --git a/x-pack/plugins/alerting/server/index.test.ts b/x-pack/plugins/alerting/server/index.test.ts new file mode 100644 index 0000000000000..45a632e0ef4ad --- /dev/null +++ b/x-pack/plugins/alerting/server/index.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { config } from './index'; +import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; + +const CONFIG_PATH = 'xpack.alerting'; +const applyStackAlertDeprecations = (settings: Record = {}) => { + const deprecations = config.deprecations!(configDeprecationFactory); + const deprecationMessages: string[] = []; + const _config = { + [CONFIG_PATH]: settings, + }; + const { config: migrated } = applyDeprecations( + _config, + deprecations.map((deprecation) => ({ + deprecation, + path: CONFIG_PATH, + })), + () => ({ message }) => deprecationMessages.push(message) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('index', () => { + describe('deprecations', () => { + it('should deprecate .enabled flag', () => { + const { messages } = applyStackAlertDeprecations({ enabled: false }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.alerting.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 4e03ad63168b3..3b4688173e9b5 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { get } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { RulesClient as RulesClientClass } from './rules_client'; import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; @@ -58,5 +58,16 @@ export const config: PluginConfigDescriptor = { 'xpack.alerts.invalidateApiKeysTask.removalDelay', 'xpack.alerting.invalidateApiKeysTask.removalDelay' ), + (settings, fromPath, addDeprecation) => { + const alerting = get(settings, fromPath); + if (alerting?.enabled === false || alerting?.enabled === true) { + addDeprecation({ + message: `"xpack.alerting.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, + correctiveActions: { + manualSteps: [`Remove "xpack.alerting.enabled" from your kibana configs.`], + }, + }); + } + }, ], }; diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index 4c5513a7fc59c..deeee970ce68a 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { PluginInitializerContext } from 'src/core/server'; -import { ConfigSchema } from './types'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { ConfigSchema, IEventLogConfig } from './types'; import { Plugin } from './plugin'; export { @@ -24,5 +24,22 @@ export { ClusterClientAdapter } from './es/cluster_client_adapter'; export { createReadySignal } from './lib/ready_signal'; -export const config = { schema: ConfigSchema }; +export const config: PluginConfigDescriptor = { + schema: ConfigSchema, + deprecations: () => [ + (settings, fromPath, addDeprecation) => { + if ( + settings?.xpack?.eventLog?.enabled === false || + settings?.xpack?.eventLog?.enabled === true + ) { + addDeprecation({ + message: `"xpack.eventLog.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, + correctiveActions: { + manualSteps: [`Remove "xpack.eventLog.enabled" from your kibana configs.`], + }, + }); + } + }, + ], +}; export const plugin = (context: PluginInitializerContext) => new Plugin(context); diff --git a/x-pack/plugins/stack_alerts/server/index.test.ts b/x-pack/plugins/stack_alerts/server/index.test.ts new file mode 100644 index 0000000000000..3d3be3acb30ea --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/index.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { config } from './index'; +import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; + +const CONFIG_PATH = 'xpack.stack_alerts'; +const applyStackAlertDeprecations = (settings: Record = {}) => { + const deprecations = config.deprecations!(configDeprecationFactory); + const deprecationMessages: string[] = []; + const _config = { + [CONFIG_PATH]: settings, + }; + const { config: migrated } = applyDeprecations( + _config, + deprecations.map((deprecation) => ({ + deprecation, + path: CONFIG_PATH, + })), + () => ({ message }) => deprecationMessages.push(message) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('index', () => { + describe('deprecations', () => { + it('should deprecate .enabled flag', () => { + const { messages } = applyStackAlertDeprecations({ enabled: false }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.stack_alerts.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index bd10a486fa531..9491f3e646c70 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { get } from 'lodash'; import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { AlertingBuiltinsPlugin } from './plugin'; import { configSchema, Config } from '../common/config'; @@ -13,6 +13,19 @@ export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_ty export const config: PluginConfigDescriptor = { exposeToBrowser: {}, schema: configSchema, + deprecations: () => [ + (settings, fromPath, addDeprecation) => { + const stackAlerts = get(settings, fromPath); + if (stackAlerts?.enabled === false || stackAlerts?.enabled === true) { + addDeprecation({ + message: `"xpack.stack_alerts.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, + correctiveActions: { + manualSteps: [`Remove "xpack.stack_alerts.enabled" from your kibana configs.`], + }, + }); + } + }, + ], }; export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx); diff --git a/x-pack/plugins/task_manager/server/index.test.ts b/x-pack/plugins/task_manager/server/index.test.ts index 8eb98c39a2ccd..9d5e42b962c55 100644 --- a/x-pack/plugins/task_manager/server/index.test.ts +++ b/x-pack/plugins/task_manager/server/index.test.ts @@ -50,4 +50,13 @@ describe('deprecations', () => { ] `); }); + + it('logs a deprecation warning for the enabled config', () => { + const { messages } = applyTaskManagerDeprecations({ enabled: true }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.task_manager.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", + ] + `); + }); }); diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index cc4217f41c5ef..a9b24fdf545a1 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -65,5 +65,16 @@ export const config: PluginConfigDescriptor = { }); } }, + (settings, fromPath, addDeprecation) => { + const taskManager = get(settings, fromPath); + if (taskManager?.enabled === false || taskManager?.enabled === true) { + addDeprecation({ + message: `"xpack.task_manager.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, + correctiveActions: { + manualSteps: [`Remove "xpack.task_manager.enabled" from your kibana configs.`], + }, + }); + } + }, ], }; diff --git a/x-pack/plugins/triggers_actions_ui/server/index.test.ts b/x-pack/plugins/triggers_actions_ui/server/index.test.ts new file mode 100644 index 0000000000000..eb0f4882a5ba8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/index.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { config } from './index'; +import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; + +const CONFIG_PATH = 'xpack.trigger_actions_ui'; +const applyStackAlertDeprecations = (settings: Record = {}) => { + const deprecations = config.deprecations!(configDeprecationFactory); + const deprecationMessages: string[] = []; + const _config = { + [CONFIG_PATH]: settings, + }; + const { config: migrated } = applyDeprecations( + _config, + deprecations.map((deprecation) => ({ + deprecation, + path: CONFIG_PATH, + })), + () => ({ message }) => deprecationMessages.push(message) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('index', () => { + describe('deprecations', () => { + it('should deprecate .enabled flag', () => { + const { messages } = applyStackAlertDeprecations({ enabled: false }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.trigger_actions_ui.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts index 7b41fa5c3df61..c7d363af45247 100644 --- a/x-pack/plugins/triggers_actions_ui/server/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { get } from 'lodash'; import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; import { configSchema, ConfigSchema } from '../config'; import { TriggersActionsPlugin } from './plugin'; @@ -26,6 +26,19 @@ export const config: PluginConfigDescriptor = { enableGeoTrackingThresholdAlert: true, }, schema: configSchema, + deprecations: () => [ + (settings, fromPath, addDeprecation) => { + const triggersActionsUi = get(settings, fromPath); + if (triggersActionsUi?.enabled === false || triggersActionsUi?.enabled === true) { + addDeprecation({ + message: `"xpack.trigger_actions_ui.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, + correctiveActions: { + manualSteps: [`Remove "xpack.trigger_actions_ui.enabled" from your kibana configs.`], + }, + }); + } + }, + ], }; export const plugin = (ctx: PluginInitializerContext) => new TriggersActionsPlugin(ctx); From 21397b65e824752fdefdbaead63e3c830e1c5b92 Mon Sep 17 00:00:00 2001 From: Davey Holler Date: Thu, 12 Aug 2021 18:55:14 -0700 Subject: [PATCH 34/91] Enterprise Search UI Copy Pass (#107812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First session of copy passes. More to come. * Setup guide copy changes * can not → cannot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../enterprise_search/common/constants.ts | 5 ++- .../components/crawler/crawler_landing.tsx | 2 +- .../components/credentials/credentials.tsx | 4 +-- .../credentials_list/credentials_list.tsx | 23 +++++++++++-- .../components/engines/constants.tsx | 4 +-- .../log_retention_confirmation_modal.tsx | 6 ++-- .../log_retention/log_retention_panel.tsx | 32 ++++++++++--------- .../components/setup_guide/setup_guide.tsx | 2 +- .../product_card/product_card.test.tsx | 6 ++-- .../components/product_card/product_card.tsx | 4 +-- .../product_selector/product_selector.tsx | 2 +- .../enterprise_search/constants.ts | 2 +- .../shared/product_button/product_button.tsx | 2 +- .../views/setup_guide/setup_guide.tsx | 2 +- 14 files changed, 58 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 3764e4d483dab..c6878c24292eb 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -46,8 +46,7 @@ export const APP_SEARCH_PLUGIN = { 'Leverage dashboards, analytics, and APIs for advanced application search made simple.', }), CARD_DESCRIPTION: i18n.translate('xpack.enterpriseSearch.appSearch.productCardDescription', { - defaultMessage: - 'Elastic App Search provides user-friendly tools to design and deploy a powerful search to your websites or web/mobile applications.', + defaultMessage: 'Design and deploy a powerful search to your websites and apps.', }), URL: '/app/enterprise_search/app_search', SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/app-search/', @@ -66,7 +65,7 @@ export const WORKPLACE_SEARCH_PLUGIN = { 'xpack.enterpriseSearch.workplaceSearch.productCardDescription', { defaultMessage: - "Unify all your team's content in one place, with instant connectivity to popular productivity and collaboration tools.", + 'Unify your content in one place, with instant connectivity to popular productivity and collaboration tools.', } ), URL: '/app/enterprise_search/workplace_search', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx index 0a53c84525a82..2afb36a40fe0f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx @@ -30,7 +30,7 @@ export const CrawlerLanding: React.FC = () => (

{i18n.translate('xpack.enterpriseSearch.appSearch.engine.crawler.landingPage.title', { - defaultMessage: 'Setup the Web Crawler', + defaultMessage: 'Set up the Web Crawler', })}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index f81d8d64737df..f777278190db2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -91,7 +91,7 @@ export const Credentials: React.FC = () => {

{i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', { - defaultMessage: 'API Keys', + defaultMessage: 'API keys', })}

@@ -105,7 +105,7 @@ export const Credentials: React.FC = () => { onClick={() => showCredentialsForm()} > {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', { - defaultMessage: 'Create a key', + defaultMessage: 'Create key', })} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index 765c8c5ea847a..040f313b12205 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -9,12 +9,19 @@ import React, { useMemo } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiBasicTable, EuiBasicTableColumn, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiButton, + EuiCopy, + EuiEmptyPrompt, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants'; import { HiddenText } from '../../../../shared/hidden_text'; import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; +import { DOCS_PREFIX } from '../../../routes'; import { TOKEN_TYPE_DISPLAY_NAMES } from '../constants'; import { CredentialsLogic } from '../credentials_logic'; import { ApiToken } from '../types'; @@ -131,8 +138,20 @@ export const CredentialsList: React.FC = () => {
+
+ + + Card title + + +
+

+ Description +

+
+
+ +

} body={i18n.translate('xpack.enterpriseSearch.appSearch.credentials.empty.body', { - defaultMessage: 'Click the "Create a key" button to make your first one.', + defaultMessage: 'Allow applications to access Elastic App Search on your behalf.', })} + actions={ + + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.empty.buttonLabel', { + defaultMessage: 'Learn about API keys', + })} + + } /> } loading={!isCredentialsDataComplete} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx index 0f1b783ddd134..99b19989a3b60 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx @@ -57,13 +57,13 @@ export const SOURCE_ENGINES_TITLE = i18n.translate( export const CREATE_AN_ENGINE_BUTTON_LABEL = i18n.translate( 'xpack.enterpriseSearch.appSearch.engines.createEngineButtonLabel', { - defaultMessage: 'Create an engine', + defaultMessage: 'Create engine', } ); export const CREATE_A_META_ENGINE_BUTTON_LABEL = i18n.translate( 'xpack.enterpriseSearch.appSearch.engines.createMetaEngineButtonLabel', { - defaultMessage: 'Create a meta engine', + defaultMessage: 'Create meta engine', } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx index ba79d62cfe615..28d4257f2487c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx @@ -20,7 +20,7 @@ export const LogRetentionConfirmationModal: React.FC = () => { const CANNOT_BE_RECOVERED_TEXT = i18n.translate( 'xpack.enterpriseSearch.appSearch.settings.logRetention.modal.recovery', { - defaultMessage: 'Once your data has been removed, it cannot be recovered.', + defaultMessage: 'You cannot recover deleted data.', } ); @@ -72,7 +72,7 @@ export const LogRetentionConfirmationModal: React.FC = () => { 'xpack.enterpriseSearch.appSearch.settings.logRetention.modal.analytics.description', { defaultMessage: - 'When disabling Analytics Logs, all your engines will immediately stop indexing Analytics Logs. Your existing data will be deleted in accordance with the storage timeframes outlined above.', + 'When you disable writing, engines stop logging analytics events. Your existing data is deleted according to the storage time frame.', } )}

@@ -117,7 +117,7 @@ export const LogRetentionConfirmationModal: React.FC = () => { 'xpack.enterpriseSearch.appSearch.settings.logRetention.modal.api.description', { defaultMessage: - 'When disabling API Logs, all your engines will immediately stop indexing API Logs. Your existing data will be deleted in accordance with the storage timeframes outlined above.', + 'When you disable writing, engines stop logging API events. Your existing data is deleted according to the storage time frame.', } )}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index fb4b503c7e62c..9012a30b950d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -42,22 +42,10 @@ export const LogRetentionPanel: React.FC = () => {

{i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.title', { - defaultMessage: 'Log Retention', + defaultMessage: 'Log retention', })}

- -

- {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.description', { - defaultMessage: 'Manage the default write settings for API Logs and Analytics.', - })}{' '} - - {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { - defaultMessage: 'Learn more about retention settings.', - })} - -

-
{ {i18n.translate( 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label', { - defaultMessage: 'Analytics Logs', + defaultMessage: 'Log analytics events', } )} @@ -94,7 +82,7 @@ export const LogRetentionPanel: React.FC = () => { {i18n.translate( 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.label', { - defaultMessage: 'API Logs', + defaultMessage: 'Log API events', } )} @@ -112,6 +100,20 @@ export const LogRetentionPanel: React.FC = () => { data-test-subj="LogRetentionPanelAPISwitch" /> + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.description', { + defaultMessage: 'Log retention is determined by the ILM policies for your deployment.', + })} +
+ + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { + defaultMessage: 'Learn more about log retention for Enterprise Search.', + })} + +

+
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index 3d96b22859fad..0fc22d8b0d406 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -50,7 +50,7 @@ export const SetupGuide: React.FC = () => (

diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index 9d54a9f1725e9..d15cb817f9064 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -32,7 +32,7 @@ describe('ProductCard', () => { const button = card.find(EuiButtonTo); expect(button.prop('to')).toEqual('/app/enterprise_search/app_search'); - expect(button.prop('children')).toEqual('Launch App Search'); + expect(button.prop('children')).toEqual('Open App Search'); button.simulate('click'); expect(mockTelemetryActions.sendEnterpriseSearchTelemetry).toHaveBeenCalledWith({ @@ -50,7 +50,7 @@ describe('ProductCard', () => { const button = card.find(EuiButtonTo); expect(button.prop('to')).toEqual('/app/enterprise_search/workplace_search'); - expect(button.prop('children')).toEqual('Launch Workplace Search'); + expect(button.prop('children')).toEqual('Open Workplace Search'); button.simulate('click'); expect(mockTelemetryActions.sendEnterpriseSearchTelemetry).toHaveBeenCalledWith({ @@ -66,6 +66,6 @@ describe('ProductCard', () => { const card = wrapper.find(EuiCard).dive().shallow(); const button = card.find(EuiButtonTo); - expect(button.prop('children')).toEqual('Setup Workplace Search'); + expect(button.prop('children')).toEqual('Set up Workplace Search'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index f0790ced414b5..c8dd9523f62f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -37,7 +37,7 @@ export const ProductCard: React.FC = ({ product, image }) => { const LAUNCH_BUTTON_TEXT = i18n.translate( 'xpack.enterpriseSearch.overview.productCard.launchButton', { - defaultMessage: 'Launch {productName}', + defaultMessage: 'Open {productName}', values: { productName: product.NAME }, } ); @@ -45,7 +45,7 @@ export const ProductCard: React.FC = ({ product, image }) => { const SETUP_BUTTON_TEXT = i18n.translate( 'xpack.enterpriseSearch.overview.productCard.setupButton', { - defaultMessage: 'Setup {productName}', + defaultMessage: 'Set up {productName}', values: { productName: product.NAME }, } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index 0dd2b0988b3f4..32ca131d6c835 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -71,7 +71,7 @@ export const ProductSelector: React.FC = ({ access }) => {

{config.host ? i18n.translate('xpack.enterpriseSearch.overview.subheading', { - defaultMessage: 'Select a product to get started.', + defaultMessage: 'Add search to your app or organization.', }) : i18n.translate('xpack.enterpriseSearch.overview.setupHeading', { defaultMessage: 'Choose a product to set up and get started.', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts index c5997222fef6e..3ab00cdd27e72 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts @@ -10,6 +10,6 @@ import { i18n } from '@kbn/i18n'; export const PRODUCT_SELECTOR_CALLOUT_HEADING = i18n.translate( 'xpack.enterpriseSearch.productSelectorCalloutTitle', { - defaultMessage: 'Enterprise-grade functionality for teams big and small', + defaultMessage: 'Enterprise-grade features for teams big and small', } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx index 3611bfb2a3f69..6199ad672f361 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx @@ -35,7 +35,7 @@ export const ProductButton: React.FC = () => { ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx index 58a80914fd3c5..05692a0ecab8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx @@ -49,7 +49,7 @@ export const SetupGuide: React.FC = () => {

From 3e542e556a4dbbf4ecbe747f4b6077999f0c0f2c Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 13 Aug 2021 08:54:46 +0300 Subject: [PATCH 35/91] [Canvas] `ESFieldSelect` refactor. (#107004) * Refactored `ESFieldSelect`. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...es_field_select.js => es_field_select.tsx} | 36 +++++++++---------- .../components/es_field_select/index.js | 33 ----------------- .../components/es_field_select/index.tsx | 29 +++++++++++++++ 3 files changed, 47 insertions(+), 51 deletions(-) rename x-pack/plugins/canvas/public/components/es_field_select/{es_field_select.js => es_field_select.tsx} (62%) delete mode 100644 x-pack/plugins/canvas/public/components/es_field_select/index.js create mode 100644 x-pack/plugins/canvas/public/components/es_field_select/index.tsx diff --git a/x-pack/plugins/canvas/public/components/es_field_select/es_field_select.js b/x-pack/plugins/canvas/public/components/es_field_select/es_field_select.tsx similarity index 62% rename from x-pack/plugins/canvas/public/components/es_field_select/es_field_select.js rename to x-pack/plugins/canvas/public/components/es_field_select/es_field_select.tsx index cbe7828ad370a..28d24423e497b 100644 --- a/x-pack/plugins/canvas/public/components/es_field_select/es_field_select.js +++ b/x-pack/plugins/canvas/public/components/es_field_select/es_field_select.tsx @@ -5,20 +5,32 @@ * 2.0. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FocusEventHandler } from 'react'; import { EuiComboBox } from '@elastic/eui'; -import { get } from 'lodash'; -export const ESFieldSelect = ({ value, fields = [], onChange, onFocus, onBlur }) => { +export interface ESFieldSelectProps { + index: string; + value: string; + onChange: (field: string | null) => void; + onBlur: FocusEventHandler | undefined; + onFocus: FocusEventHandler | undefined; + fields: string[]; +} + +export const ESFieldSelect: React.FunctionComponent = ({ + value, + fields = [], + onChange, + onFocus, + onBlur, +}) => { const selectedOption = value ? [{ label: value }] : []; const options = fields.map((field) => ({ label: field })); - return ( onChange(get(field, 'label', null))} + onChange={([field]) => onChange(field?.label ?? null)} onSearchChange={(searchValue) => { // resets input when user starts typing if (searchValue) { @@ -33,15 +45,3 @@ export const ESFieldSelect = ({ value, fields = [], onChange, onFocus, onBlur }) /> ); }; - -ESFieldSelect.propTypes = { - onChange: PropTypes.func, - onFocus: PropTypes.func, - onBlur: PropTypes.func, - value: PropTypes.string, - fields: PropTypes.array, -}; - -ESFieldSelect.defaultProps = { - fields: [], -}; diff --git a/x-pack/plugins/canvas/public/components/es_field_select/index.js b/x-pack/plugins/canvas/public/components/es_field_select/index.js deleted file mode 100644 index bbeefb73d3b83..0000000000000 --- a/x-pack/plugins/canvas/public/components/es_field_select/index.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { compose, withState, lifecycle } from 'recompose'; -import { getFields } from '../../lib/es_service'; -import { ESFieldSelect as Component } from './es_field_select'; - -export const ESFieldSelect = compose( - withState('fields', 'setFields', []), - lifecycle({ - componentDidMount() { - if (this.props.index) { - getFields(this.props.index).then(this.props.setFields); - } - }, - componentDidUpdate({ index }) { - const { value, onChange, setFields } = this.props; - if (this.props.index !== index) { - getFields(this.props.index).then((fields) => { - setFields(fields); - }); - } - - if (value && !this.props.fields.includes(value)) { - onChange(null); - } - }, - }) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/es_field_select/index.tsx b/x-pack/plugins/canvas/public/components/es_field_select/index.tsx new file mode 100644 index 0000000000000..3b3d5aa13fd24 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/es_field_select/index.tsx @@ -0,0 +1,29 @@ +/* + * 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, useEffect } from 'react'; +import { getFields } from '../../lib/es_service'; +import { ESFieldSelect as Component, ESFieldSelectProps as Props } from './es_field_select'; + +type ESFieldSelectProps = Omit; + +export const ESFieldSelect: React.FunctionComponent = (props) => { + const { index, value, onChange } = props; + const [fields, setFields] = useState([]); + + useEffect(() => { + getFields(index).then((newFields) => setFields(newFields || [])); + }, [index]); + + useEffect(() => { + if (value && !fields.includes(value)) { + onChange(null); + } + }, [value, fields, onChange]); + + return ; +}; From ac0d785eba51216ad0d7c2e50763f276ccee1c1f Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 13 Aug 2021 08:55:40 +0300 Subject: [PATCH 36/91] [Canvas] `DatasourcePreview` refactor. (#106644) * Moved `DatasourcePreview` from `recompose` to `hooks`. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../datasource/datasource_preview/index.js | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js index 75a2ecc98cdae..89faef29a3b02 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js @@ -5,28 +5,25 @@ * 2.0. */ -import { pure, compose, lifecycle, withState, branch, renderComponent } from 'recompose'; +import React, { useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; import { interpretAst } from '../../../lib/run_interpreter'; import { Loading } from '../../loading'; import { DatasourcePreview as Component } from './datasource_preview'; -export const DatasourcePreview = compose( - pure, - withState('datatable', 'setDatatable'), - lifecycle({ - componentDidMount() { - interpretAst( - { - type: 'expression', - chain: [this.props.function], - }, - {} - ).then(this.props.setDatatable); - }, - }), - branch(({ datatable }) => !datatable, renderComponent(Loading)) -)(Component); +export const DatasourcePreview = (props) => { + const [datatable, setDatatable] = useState(); + + useEffect(() => { + interpretAst({ type: 'expression', chain: [props.function] }, {}).then(setDatatable); + }, [props.function, setDatatable]); + + if (!datatable) { + return ; + } + + return ; +}; DatasourcePreview.propTypes = { function: PropTypes.object, From dc1ceefbfd8260c5a1e67b8bf0eaec39ac73955e Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 13 Aug 2021 09:26:42 +0200 Subject: [PATCH 37/91] Bump Node.js from version 14.17.3 to 14.17.5. (#108324) --- .ci/Dockerfile | 2 +- .node-version | 2 +- .nvmrc | 2 +- WORKSPACE.bazel | 12 ++++++------ package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 8568201a2805d..947242ecc0ece 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=14.17.3 +ARG NODE_VERSION=14.17.5 FROM node:${NODE_VERSION} AS base diff --git a/.node-version b/.node-version index c6244cda0441f..18711d290eac4 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.17.3 +14.17.5 diff --git a/.nvmrc b/.nvmrc index c6244cda0441f..18711d290eac4 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.17.3 +14.17.5 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index e26c2ec09acf7..a3c0cd76250e0 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.7.0") # we can update that rule. node_repositories( node_repositories = { - "14.17.3-darwin_amd64": ("node-v14.17.3-darwin-x64.tar.gz", "node-v14.17.3-darwin-x64", "522f85db1d1fe798cba5f601d1bba7b5203ca8797b2bc934ff6f24263f0b7fb2"), - "14.17.3-linux_arm64": ("node-v14.17.3-linux-arm64.tar.xz", "node-v14.17.3-linux-arm64", "80f4143d3c2d4cf3c4420eea3202c7bf16788b0a72fd512e60bfc8066a08a51c"), - "14.17.3-linux_s390x": ("node-v14.17.3-linux-s390x.tar.xz", "node-v14.17.3-linux-s390x", "4f69c30732f94189b9ab98f3100b17f1e4db2000848d56064e887be1c28e81ae"), - "14.17.3-linux_amd64": ("node-v14.17.3-linux-x64.tar.xz", "node-v14.17.3-linux-x64", "d659d78144042a1801f35dd611d0fab137e841cde902b2c6a821163a5e36f105"), - "14.17.3-windows_amd64": ("node-v14.17.3-win-x64.zip", "node-v14.17.3-win-x64", "170fb4f95539d1d7e1295fb2556cb72bee352cdf81a02ffb16cf6d50ad2fefbf"), + "14.17.5-darwin_amd64": ("node-v14.17.5-darwin-x64.tar.gz", "node-v14.17.5-darwin-x64", "2e40ab625b45b9bdfcb963ddd4d65d87ddf1dd37a86b6f8b075cf3d77fe9dc09"), + "14.17.5-linux_arm64": ("node-v14.17.5-linux-arm64.tar.xz", "node-v14.17.5-linux-arm64", "3a2e674b6db50dfde767c427e8f077235bbf6f9236e1b12a4cc3496b12f94bae"), + "14.17.5-linux_s390x": ("node-v14.17.5-linux-s390x.tar.xz", "node-v14.17.5-linux-s390x", "7d40eee3d54241403db12fb3bc420cd776e2b02e89100c45cf5e74a73942e7f6"), + "14.17.5-linux_amd64": ("node-v14.17.5-linux-x64.tar.xz", "node-v14.17.5-linux-x64", "2d759de07a50cd7f75bd73d67e97b0d0e095ee3c413efac7d1b3d1e84ed76fff"), + "14.17.5-windows_amd64": ("node-v14.17.5-win-x64.zip", "node-v14.17.5-win-x64", "a99b7ee08e846e5d1f4e70c4396265542819d79ed9cebcc27760b89571f03cbf"), }, - node_version = "14.17.3", + node_version = "14.17.5", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/package.json b/package.json index d7a072d1caef0..70c89c197eeb9 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "**/underscore": "^1.13.1" }, "engines": { - "node": "14.17.3", + "node": "14.17.5", "yarn": "^1.21.1" }, "dependencies": { From 8cc5f49eebaea6bd0540dafe4609aa5eda289624 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Fri, 13 Aug 2021 09:54:58 +0200 Subject: [PATCH 38/91] [Security Solution] Move Endpoint details flyout and related middleware to their own functions (#108330) * Split function to load endpoint details using parameters * Add action to load endpoint details * Moving all the endpoint details content to a separate component * Rename endpoint details to endpoint details content * Rename temporal file into EndpointDetails * Remove unused dispatch * Refactor ingestPolicies dispatching in the middleware --- .../pages/endpoint_hosts/store/action.ts | 8 + .../pages/endpoint_hosts/store/middleware.ts | 170 ++++---- .../view/details/endpoint_details.tsx | 396 +++++++++--------- .../view/details/endpoint_details_content.tsx | 210 ++++++++++ .../endpoint_hosts/view/details/index.tsx | 218 +--------- 5 files changed, 519 insertions(+), 483 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index f54e7d4774128..ba07cd3c9d461 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -166,6 +166,13 @@ export interface EndpointDetailsActivityLogUpdatePaging { }; } +export interface EndpointDetailsLoad { + type: 'endpointDetailsLoad'; + payload: { + endpointId: string; + }; +} + export interface EndpointDetailsActivityLogUpdateIsInvalidDateRange { type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange'; payload: { @@ -187,6 +194,7 @@ export type EndpointAction = | EndpointDetailsActivityLogUpdatePaging | EndpointDetailsActivityLogUpdateIsInvalidDateRange | EndpointDetailsActivityLogChanged + | EndpointDetailsLoad | ServerReturnedEndpointPolicyResponse | ServerFailedToReturnEndpointPolicyResponse | ServerReturnedPoliciesForOnboarding diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 19658631e6cf2..063dcc44df2e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -115,6 +115,10 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory; coreStart: CoreStart; + selectedEndpoint: string; }) { const { getState, dispatch } = store; - dispatch({ - type: 'serverCancelledPolicyItemsLoading', - }); - - // If user navigated directly to a endpoint details page, load the endpoint list - if (listData(getState()).length === 0) { - const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState()); - try { - const response = await coreStart.http.post(HOST_METADATA_LIST_ROUTE, { - body: JSON.stringify({ - paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], - }), - }); - response.request_page_index = Number(pageIndex); - dispatch({ - type: 'serverReturnedEndpointList', - payload: response, - }); - - try { - const ingestPolicies = await getAgentAndPoliciesForEndpointsList( - coreStart.http, - response.hosts, - nonExistingPolicies(getState()) - ); - if (ingestPolicies?.packagePolicy !== undefined) { - dispatch({ - type: 'serverReturnedEndpointNonExistingPolicies', - payload: ingestPolicies.packagePolicy, - }); - } - if (ingestPolicies?.agentPolicy !== undefined) { - dispatch({ - type: 'serverReturnedEndpointAgentPolicies', - payload: ingestPolicies.agentPolicy, - }); - } - } catch (error) { - // TODO should handle the error instead of logging it to the browser - // Also this is an anti-pattern we shouldn't use - // Ignore Errors, since this should not hinder the user's ability to use the UI - logError(error); - } - } catch (error) { - dispatch({ - type: 'serverFailedToReturnEndpointList', - payload: error, - }); - } - } else { - dispatch({ - type: 'serverCancelledEndpointListLoading', - }); - } - // call the endpoint details api - const { selected_endpoint: selectedEndpoint } = uiQueryParams(getState()); try { const response = await coreStart.http.get( resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: selectedEndpoint as string }) @@ -736,6 +663,51 @@ async function endpointDetailsMiddleware({ }); } } + +async function endpointDetailsMiddleware({ + store, + coreStart, +}: { + store: ImmutableMiddlewareAPI; + coreStart: CoreStart; +}) { + const { getState, dispatch } = store; + dispatch({ + type: 'serverCancelledPolicyItemsLoading', + }); + + // If user navigated directly to a endpoint details page, load the endpoint list + if (listData(getState()).length === 0) { + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState()); + try { + const response = await coreStart.http.post(HOST_METADATA_LIST_ROUTE, { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + }), + }); + response.request_page_index = Number(pageIndex); + dispatch({ + type: 'serverReturnedEndpointList', + payload: response, + }); + + dispatchIngestPolicies({ http: coreStart.http, hosts: response.hosts, store }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnEndpointList', + payload: error, + }); + } + } else { + dispatch({ + type: 'serverCancelledEndpointListLoading', + }); + } + const { selected_endpoint: selectedEndpoint } = uiQueryParams(getState()); + if (selectedEndpoint !== undefined) { + loadEndpointDetails({ store, coreStart, selectedEndpoint }); + } +} async function endpointDetailsActivityLogChangedMiddleware({ store, coreStart, @@ -803,3 +775,39 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E }); } } + +async function dispatchIngestPolicies({ + store, + hosts, + http, +}: { + store: EndpointPageStore; + hosts: HostResultList['hosts']; + http: HttpStart; +}) { + const { getState, dispatch } = store; + try { + const ingestPolicies = await getAgentAndPoliciesForEndpointsList( + http, + hosts, + nonExistingPolicies(getState()) + ); + if (ingestPolicies?.packagePolicy !== undefined) { + dispatch({ + type: 'serverReturnedEndpointNonExistingPolicies', + payload: ingestPolicies.packagePolicy, + }); + } + if (ingestPolicies?.agentPolicy !== undefined) { + dispatch({ + type: 'serverReturnedEndpointAgentPolicies', + payload: ingestPolicies.agentPolicy, + }); + } + } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use + // Ignore Errors, since this should not hinder the user's ability to use the UI + logError(error); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx index 369b4c128e052..635a6ba6cf193 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx @@ -4,207 +4,223 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import styled from 'styled-components'; import { - EuiDescriptionList, - EuiListGroup, - EuiListGroupItem, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiBadge, + EuiEmptyPrompt, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiLoadingContent, EuiSpacer, + EuiText, } from '@elastic/eui'; -import React, { memo, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { isPolicyOutOfDate } from '../../utils'; -import { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types'; -import { useEndpointSelector } from '../hooks'; -import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; -import { POLICY_STATUS_TO_BADGE_COLOR } from '../host_constants'; -import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; -import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { memo, useCallback, useEffect, useMemo } from 'react'; +import { HostMetadata } from '../../../../../../common/endpoint/types'; +import { PreferenceFormattedDateFromPrimitive } from '../../../../../common/components/formatted_date'; +import { useToasts } from '../../../../../common/lib/kibana'; import { getEndpointDetailsPath } from '../../../../common/routing'; -import { EndpointPolicyLink } from '../components/endpoint_policy_link'; -import { OutOfDate } from '../components/out_of_date'; -import { EndpointAgentStatus } from '../components/endpoint_agent_status'; -import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; - -const HostIds = styled(EuiListGroupItem)` - margin-top: 0; - .euiListGroupItem__text { - padding: 0; - } -`; +import { + detailsData, + detailsError, + getActivityLogData, + hostStatusInfo, + policyResponseActions, + policyResponseAppliedRevision, + policyResponseConfigurations, + policyResponseError, + policyResponseFailedOrWarningActionCount, + policyResponseLoading, + policyResponseTimestamp, + policyVersionInfo, + showView, + uiQueryParams, +} from '../../store/selectors'; +import { useEndpointSelector } from '../hooks'; +import * as i18 from '../translations'; +import { ActionsMenu } from './components/actions_menu'; +import { BackToEndpointDetailsFlyoutSubHeader } from './components/back_to_endpoint_details_flyout_subheader'; +import { + EndpointDetailsFlyoutTabs, + EndpointDetailsTabsTypes, +} from './components/endpoint_details_tabs'; +import { EndpointIsolationFlyoutPanel } from './components/endpoint_isolate_flyout_panel'; +import { FlyoutBodyNoTopPadding } from './components/flyout_body_no_top_padding'; +import { EndpointDetailsFlyoutHeader } from './components/flyout_header'; +import { EndpointActivityLog } from './endpoint_activity_log'; +import { EndpointDetailsContent } from './endpoint_details_content'; +import { PolicyResponse } from './policy_response'; -export const EndpointDetails = memo( - ({ - details, - policyInfo, - hostStatus, - }: { - details: HostMetadata; - policyInfo?: HostInfo['policy_info']; - hostStatus: HostStatus; - }) => { - const queryParams = useEndpointSelector(uiQueryParams); - const policyStatus = useEndpointSelector( - policyResponseStatus - ) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR; - const { getAppUrl } = useAppUrl(); +export const EndpointDetails = memo(() => { + const toasts = useToasts(); + const queryParams = useEndpointSelector(uiQueryParams); - const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { selected_endpoint, show, ...currentUrlParams } = queryParams; - const path = getEndpointDetailsPath({ - name: 'endpointPolicyResponse', - ...currentUrlParams, - selected_endpoint: details.agent.id, - }); - return [getAppUrl({ path }), path]; - }, [details.agent.id, getAppUrl, queryParams]); + const activityLog = useEndpointSelector(getActivityLogData); + const hostDetails = useEndpointSelector(detailsData); + const hostDetailsError = useEndpointSelector(detailsError); - const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); - - const detailsResults = useMemo(() => { - return [ - { - title: i18n.translate('xpack.securitySolution.endpoint.details.os', { - defaultMessage: 'OS', - }), - description: {details.host.os.full}, - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.agentStatus', { - defaultMessage: 'Agent Status', - }), - description: , - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.lastSeen', { - defaultMessage: 'Last Seen', - }), - description: ( - - {' '} - - - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.policy', { - defaultMessage: 'Policy', - }), - description: ( - - - - - {details.Endpoint.policy.applied.name} - - - - - {details.Endpoint.policy.applied.endpoint_policy_version && ( - - - - - - )} - {isPolicyOutOfDate(details.Endpoint.policy.applied, policyInfo) && ( - - - - )} - - - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', { - defaultMessage: 'Policy Status', - }), - description: ( - // https://github.com/elastic/eui/issues/4530 - // @ts-ignore - - - - - - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', { - defaultMessage: 'Endpoint Version', - }), - description: {details.agent.version}, - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', { - defaultMessage: 'IP Address', - }), - description: ( - - - {details.host.ip.map((ip: string, index: number) => ( - - ))} - - - ), - }, - ]; - }, [ - details, - hostStatus, - policyResponseUri, - policyStatus, - policyStatusClickHandler, - policyInfo, - ]); + const policyInfo = useEndpointSelector(policyVersionInfo); + const hostStatus = useEndpointSelector(hostStatusInfo); + const show = useEndpointSelector(showView); - return ( + const ContentLoadingMarkup = useMemo( + () => ( <> + - + - ); - } -); + ), + [] + ); + + const getTabs = useCallback( + (id: string) => [ + { + id: EndpointDetailsTabsTypes.overview, + name: i18.OVERVIEW, + route: getEndpointDetailsPath({ + ...queryParams, + name: 'endpointDetails', + selected_endpoint: id, + }), + content: + hostDetails === undefined ? ( + ContentLoadingMarkup + ) : ( + + ), + }, + { + id: EndpointDetailsTabsTypes.activityLog, + name: i18.ACTIVITY_LOG.tabTitle, + route: getEndpointDetailsPath({ + ...queryParams, + name: 'endpointActivityLog', + selected_endpoint: id, + }), + content: , + }, + ], + [ContentLoadingMarkup, hostDetails, policyInfo, hostStatus, activityLog, queryParams] + ); + + const showFlyoutFooter = + show === 'details' || show === 'policy_response' || show === 'activity_log'; + + useEffect(() => { + if (hostDetailsError !== undefined) { + toasts.addDanger({ + title: i18n.translate('xpack.securitySolution.endpoint.details.errorTitle', { + defaultMessage: 'Could not find host', + }), + text: i18n.translate('xpack.securitySolution.endpoint.details.errorBody', { + defaultMessage: 'Please exit the flyout and select an available host.', + }), + }); + } + }, [hostDetailsError, show, toasts]); + return ( + <> + {(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && ( + + )} + {hostDetails === undefined ? ( + + + + ) : ( + <> + {(show === 'details' || show === 'activity_log') && ( + + )} + + {show === 'policy_response' && } + + {(show === 'isolate' || show === 'unisolate') && ( + + )} + + {showFlyoutFooter && ( + + + + )} + + )} + + ); +}); EndpointDetails.displayName = 'EndpointDetails'; + +const PolicyResponseFlyoutPanel = memo<{ + hostMeta: HostMetadata; +}>(({ hostMeta }) => { + const responseConfig = useEndpointSelector(policyResponseConfigurations); + const responseActions = useEndpointSelector(policyResponseActions); + const responseAttentionCount = useEndpointSelector(policyResponseFailedOrWarningActionCount); + const loading = useEndpointSelector(policyResponseLoading); + const error = useEndpointSelector(policyResponseError); + const responseTimestamp = useEndpointSelector(policyResponseTimestamp); + const responsePolicyRevisionNumber = useEndpointSelector(policyResponseAppliedRevision); + + return ( + <> + + + + +

+ +

+
+ + + , + }} + /> + + + {error && ( + + } + /> + )} + {loading && } + {responseConfig !== undefined && responseActions !== undefined && ( + + )} +
+ + ); +}); + +PolicyResponseFlyoutPanel.displayName = 'PolicyResponseFlyoutPanel'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx new file mode 100644 index 0000000000000..cc1cad52eb21c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details_content.tsx @@ -0,0 +1,210 @@ +/* + * 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 styled from 'styled-components'; +import { + EuiDescriptionList, + EuiListGroup, + EuiListGroupItem, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiSpacer, +} from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { isPolicyOutOfDate } from '../../utils'; +import { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types'; +import { useEndpointSelector } from '../hooks'; +import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; +import { POLICY_STATUS_TO_BADGE_COLOR } from '../host_constants'; +import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; +import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { getEndpointDetailsPath } from '../../../../common/routing'; +import { EndpointPolicyLink } from '../components/endpoint_policy_link'; +import { OutOfDate } from '../components/out_of_date'; +import { EndpointAgentStatus } from '../components/endpoint_agent_status'; +import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; + +const HostIds = styled(EuiListGroupItem)` + margin-top: 0; + .euiListGroupItem__text { + padding: 0; + } +`; + +export const EndpointDetailsContent = memo( + ({ + details, + policyInfo, + hostStatus, + }: { + details: HostMetadata; + policyInfo?: HostInfo['policy_info']; + hostStatus: HostStatus; + }) => { + const queryParams = useEndpointSelector(uiQueryParams); + const policyStatus = useEndpointSelector( + policyResponseStatus + ) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR; + const { getAppUrl } = useAppUrl(); + + const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { selected_endpoint, show, ...currentUrlParams } = queryParams; + const path = getEndpointDetailsPath({ + name: 'endpointPolicyResponse', + ...currentUrlParams, + selected_endpoint: details.agent.id, + }); + return [getAppUrl({ path }), path]; + }, [details.agent.id, getAppUrl, queryParams]); + + const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); + + const detailsResults = useMemo(() => { + return [ + { + title: i18n.translate('xpack.securitySolution.endpoint.details.os', { + defaultMessage: 'OS', + }), + description: {details.host.os.full}, + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.agentStatus', { + defaultMessage: 'Agent Status', + }), + description: , + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.lastSeen', { + defaultMessage: 'Last Seen', + }), + description: ( + + {' '} + + + ), + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.policy', { + defaultMessage: 'Policy', + }), + description: ( + + + + + {details.Endpoint.policy.applied.name} + + + + + {details.Endpoint.policy.applied.endpoint_policy_version && ( + + + + + + )} + {isPolicyOutOfDate(details.Endpoint.policy.applied, policyInfo) && ( + + + + )} + + + ), + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', { + defaultMessage: 'Policy Status', + }), + description: ( + // https://github.com/elastic/eui/issues/4530 + // @ts-ignore + + + + + + ), + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', { + defaultMessage: 'Endpoint Version', + }), + description: {details.agent.version}, + }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', { + defaultMessage: 'IP Address', + }), + description: ( + + + {details.host.ip.map((ip: string, index: number) => ( + + ))} + + + ), + }, + ]; + }, [ + details, + hostStatus, + policyResponseUri, + policyStatus, + policyStatusClickHandler, + policyInfo, + ]); + + return ( + <> + + + + ); + } +); + +EndpointDetailsContent.displayName = 'EndpointDetailsContent'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index d3fc812f6317b..84b0bdad4d6eb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -4,122 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import React, { useCallback, useEffect, useMemo, memo } from 'react'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiLoadingContent, - EuiText, - EuiSpacer, - EuiEmptyPrompt, -} from '@elastic/eui'; +import React, { useCallback, memo } from 'react'; +import { EuiFlyout } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { useToasts } from '../../../../../common/lib/kibana'; import { useEndpointSelector } from '../hooks'; -import { - uiQueryParams, - detailsData, - detailsError, - getActivityLogData, - showView, - policyResponseConfigurations, - policyResponseActions, - policyResponseFailedOrWarningActionCount, - policyResponseError, - policyResponseLoading, - policyResponseTimestamp, - policyVersionInfo, - hostStatusInfo, - policyResponseAppliedRevision, -} from '../../store/selectors'; -import { EndpointDetails } from './endpoint_details'; -import { EndpointActivityLog } from './endpoint_activity_log'; -import { PolicyResponse } from './policy_response'; -import * as i18 from '../translations'; -import { HostMetadata } from '../../../../../../common/endpoint/types'; -import { - EndpointDetailsFlyoutTabs, - EndpointDetailsTabsTypes, -} from './components/endpoint_details_tabs'; +import { uiQueryParams } from '../../store/selectors'; -import { PreferenceFormattedDateFromPrimitive } from '../../../../../common/components/formatted_date'; -import { EndpointIsolationFlyoutPanel } from './components/endpoint_isolate_flyout_panel'; -import { BackToEndpointDetailsFlyoutSubHeader } from './components/back_to_endpoint_details_flyout_subheader'; -import { FlyoutBodyNoTopPadding } from './components/flyout_body_no_top_padding'; -import { getEndpointListPath, getEndpointDetailsPath } from '../../../../common/routing'; -import { ActionsMenu } from './components/actions_menu'; -import { EndpointDetailsFlyoutHeader } from './components/flyout_header'; +import { getEndpointListPath } from '../../../../common/routing'; +import { EndpointDetails } from './endpoint_details'; export const EndpointDetailsFlyout = memo(() => { const history = useHistory(); - const toasts = useToasts(); const queryParams = useEndpointSelector(uiQueryParams); const { selected_endpoint: selectedEndpoint, ...queryParamsWithoutSelectedEndpoint } = queryParams; - const activityLog = useEndpointSelector(getActivityLogData); - const hostDetails = useEndpointSelector(detailsData); - const hostDetailsError = useEndpointSelector(detailsError); - - const policyInfo = useEndpointSelector(policyVersionInfo); - const hostStatus = useEndpointSelector(hostStatusInfo); - const show = useEndpointSelector(showView); - - const ContentLoadingMarkup = useMemo( - () => ( - <> - - - - - ), - [] - ); - - const getTabs = useCallback( - (id: string) => [ - { - id: EndpointDetailsTabsTypes.overview, - name: i18.OVERVIEW, - route: getEndpointDetailsPath({ - ...queryParams, - name: 'endpointDetails', - selected_endpoint: id, - }), - content: - hostDetails === undefined ? ( - ContentLoadingMarkup - ) : ( - - ), - }, - { - id: EndpointDetailsTabsTypes.activityLog, - name: i18.ACTIVITY_LOG.tabTitle, - route: getEndpointDetailsPath({ - ...queryParams, - name: 'endpointActivityLog', - selected_endpoint: id, - }), - content: , - }, - ], - [ContentLoadingMarkup, hostDetails, policyInfo, hostStatus, activityLog, queryParams] - ); - - const showFlyoutFooter = - show === 'details' || show === 'policy_response' || show === 'activity_log'; - const handleFlyoutClose = useCallback(() => { const { show: _show, ...urlSearchParams } = queryParamsWithoutSelectedEndpoint; history.push( @@ -129,20 +30,6 @@ export const EndpointDetailsFlyout = memo(() => { }) ); }, [history, queryParamsWithoutSelectedEndpoint]); - - useEffect(() => { - if (hostDetailsError !== undefined) { - toasts.addDanger({ - title: i18n.translate('xpack.securitySolution.endpoint.details.errorTitle', { - defaultMessage: 'Could not find host', - }), - text: i18n.translate('xpack.securitySolution.endpoint.details.errorBody', { - defaultMessage: 'Please exit the flyout and select an available host.', - }), - }); - } - }, [hostDetailsError, show, toasts]); - return ( { paddingSize="l" ownFocus={false} > - {(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && ( - - )} - {hostDetails === undefined ? ( - - - - ) : ( - <> - {(show === 'details' || show === 'activity_log') && ( - - )} - - {show === 'policy_response' && } - - {(show === 'isolate' || show === 'unisolate') && ( - - )} - - {showFlyoutFooter && ( - - - - )} - - )} + ); }); EndpointDetailsFlyout.displayName = 'EndpointDetailsFlyout'; - -const PolicyResponseFlyoutPanel = memo<{ - hostMeta: HostMetadata; -}>(({ hostMeta }) => { - const responseConfig = useEndpointSelector(policyResponseConfigurations); - const responseActions = useEndpointSelector(policyResponseActions); - const responseAttentionCount = useEndpointSelector(policyResponseFailedOrWarningActionCount); - const loading = useEndpointSelector(policyResponseLoading); - const error = useEndpointSelector(policyResponseError); - const responseTimestamp = useEndpointSelector(policyResponseTimestamp); - const responsePolicyRevisionNumber = useEndpointSelector(policyResponseAppliedRevision); - - return ( - <> - - - - -

- -

-
- - - , - }} - /> - - - {error && ( - - } - /> - )} - {loading && } - {responseConfig !== undefined && responseActions !== undefined && ( - - )} -
- - ); -}); - -PolicyResponseFlyoutPanel.displayName = 'PolicyResponseFlyoutPanel'; From ebdda25fa86e0af1c0244f87da2a5dcf74c928e0 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Fri, 13 Aug 2021 11:44:07 +0200 Subject: [PATCH 39/91] migrationsv2: handle 413 errors and log the request details for unexpected ES failures (#108213) * Log the failing request and response code when an action throws a response error * Provide useful log message when migrations fail due to ES 413 Request Entity Too Large * Don't log request body for unexpected ES request failures * Fix types * CR feedback: fix order of ES request debug log Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../elasticsearch/client/configure_client.ts | 40 ++++++++++---- src/core/server/elasticsearch/client/index.ts | 2 +- src/core/server/elasticsearch/index.ts | 1 + .../bulk_overwrite_transformed_documents.ts | 13 +++-- .../migrationsv2/actions/index.ts | 5 ++ .../actions/integration_tests/actions.test.ts | 53 ++++++++++++++----- .../migrations_state_action_machine.test.ts | 7 ++- .../migrations_state_action_machine.ts | 20 ++++--- .../migrationsv2/model/model.test.ts | 21 ++++++++ .../saved_objects/migrationsv2/model/model.ts | 20 ++++++- 10 files changed, 144 insertions(+), 38 deletions(-) diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index 3c32dd2cfd4f4..631e20ac238f1 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -72,11 +72,13 @@ function ensureString(body: RequestBody): string { return JSON.stringify(body); } -function getErrorMessage(error: ApiError, event: RequestEvent): string { +/** + * Returns a debug message from an Elasticsearch error in the following format: + * [error type] error reason + */ +export function getErrorMessage(error: ApiError): string { if (error instanceof errors.ResponseError) { - return `${getResponseMessage(event)} [${event.body?.error?.type}]: ${ - event.body?.error?.reason ?? error.message - }`; + return `[${error.meta.body?.error?.type}]: ${error.meta.body?.error?.reason ?? error.message}`; } return `[${error.name}]: ${error.message}`; } @@ -85,19 +87,33 @@ function getErrorMessage(error: ApiError, event: RequestEvent): string { * returns a string in format: * * status code - * URL + * method URL * request body * * so it could be copy-pasted into the Dev console */ function getResponseMessage(event: RequestEvent): string { - const params = event.meta.request.params; + const errorMeta = getRequestDebugMeta(event); + const body = errorMeta.body ? `\n${errorMeta.body}` : ''; + return `${errorMeta.statusCode}\n${errorMeta.method} ${errorMeta.url}${body}`; +} +/** + * Returns stringified debug information from an Elasticsearch request event + * useful for logging in case of an unexpected failure. + */ +export function getRequestDebugMeta( + event: RequestEvent +): { url: string; body: string; statusCode: number | null; method: string } { + const params = event.meta.request.params; // definition is wrong, `params.querystring` can be either a string or an object const querystring = convertQueryString(params.querystring); - const url = `${params.path}${querystring ? `?${querystring}` : ''}`; - const body = params.body ? `\n${ensureString(params.body)}` : ''; - return `${event.statusCode}\n${params.method} ${url}${body}`; + return { + url: `${params.path}${querystring ? `?${querystring}` : ''}`, + body: params.body ? `${ensureString(params.body)}` : '', + method: params.method, + statusCode: event.statusCode, + }; } const addLogging = (client: Client, logger: Logger) => { @@ -110,7 +126,11 @@ const addLogging = (client: Client, logger: Logger) => { } : undefined; // do not clutter logs if opaqueId is not present if (error) { - logger.debug(getErrorMessage(error, event), meta); + if (error instanceof errors.ResponseError) { + logger.debug(`${getResponseMessage(event)} ${getErrorMessage(error)}`, meta); + } else { + logger.debug(getErrorMessage(error), meta); + } } else { logger.debug(getResponseMessage(event), meta); } diff --git a/src/core/server/elasticsearch/client/index.ts b/src/core/server/elasticsearch/client/index.ts index c7600b723ade0..29f8b85695190 100644 --- a/src/core/server/elasticsearch/client/index.ts +++ b/src/core/server/elasticsearch/client/index.ts @@ -20,5 +20,5 @@ export type { IScopedClusterClient } from './scoped_cluster_client'; export type { ElasticsearchClientConfig } from './client_config'; export { ClusterClient } from './cluster_client'; export type { IClusterClient, ICustomClusterClient } from './cluster_client'; -export { configureClient } from './configure_client'; +export { configureClient, getRequestDebugMeta, getErrorMessage } from './configure_client'; export { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 8bcc841669fc9..62bb30452bb98 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -37,4 +37,5 @@ export type { GetResponse, DeleteDocumentResponse, } from './client'; +export { getRequestDebugMeta, getErrorMessage } from './client'; export { isSupportedEsServer } from './supported_server_response_check'; diff --git a/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts b/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts index 4c0f8717576ac..d0259f8f21ca4 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts @@ -8,7 +8,7 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; -import type { estypes } from '@elastic/elasticsearch'; +import { errors as esErrors, estypes } from '@elastic/elasticsearch'; import { ElasticsearchClient } from '../../../elasticsearch'; import type { SavedObjectsRawDoc } from '../../serialization'; import { @@ -17,7 +17,7 @@ import { } from './catch_retryable_es_client_errors'; import { isWriteBlockException } from './es_errors'; import { WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE } from './constants'; -import type { TargetIndexHadWriteBlock } from './index'; +import type { TargetIndexHadWriteBlock, RequestEntityTooLargeException } from './index'; /** @internal */ export interface BulkOverwriteTransformedDocumentsParams { @@ -37,7 +37,7 @@ export const bulkOverwriteTransformedDocuments = ({ transformedDocs, refresh = false, }: BulkOverwriteTransformedDocumentsParams): TaskEither.TaskEither< - RetryableEsClientError | TargetIndexHadWriteBlock, + RetryableEsClientError | TargetIndexHadWriteBlock | RequestEntityTooLargeException, 'bulk_index_succeeded' > => () => { return client @@ -90,5 +90,12 @@ export const bulkOverwriteTransformedDocuments = ({ throw new Error(JSON.stringify(errors)); } }) + .catch((error) => { + if (error instanceof esErrors.ResponseError && error.statusCode === 413) { + return Either.left({ type: 'request_entity_too_large_exception' as const }); + } else { + throw error; + } + }) .catch(catchRetryableEsClientErrors); }; diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 97ffcff25dd76..158d97f3b7c27 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -120,6 +120,10 @@ export interface TargetIndexHadWriteBlock { type: 'target_index_had_write_block'; } +export interface RequestEntityTooLargeException { + type: 'request_entity_too_large_exception'; +} + /** @internal */ export interface AcknowledgeResponse { acknowledged: boolean; @@ -136,6 +140,7 @@ export interface ActionErrorTypeMap { alias_not_found_exception: AliasNotFound; remove_index_not_a_concrete_index: RemoveIndexNotAConcreteIndex; documents_transform_failed: DocumentsTransformFailed; + request_entity_too_large_exception: RequestEntityTooLargeException; } /** diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts index 052316e7944ce..65be45e49190d 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts @@ -46,6 +46,12 @@ import { TaskEither } from 'fp-ts/lib/TaskEither'; const { startES } = kbnTestServer.createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + esArgs: ['http.max_content_length=10Kb'], + }, + }, }); let esServer: kbnTestServer.TestElasticsearchUtils; @@ -1472,11 +1478,11 @@ describe('migration actions', () => { }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "bulk_index_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "bulk_index_succeeded", + } + `); }); it('resolves right even if there were some version_conflict_engine_exception', async () => { const existingDocs = ((await searchForOutdatedDocuments(client, { @@ -1501,7 +1507,7 @@ describe('migration actions', () => { } `); }); - it('resolves left if there are write_block errors', async () => { + it('resolves left target_index_had_write_block if there are write_block errors', async () => { const newDocs = ([ { _source: { title: 'doc 5' } }, { _source: { title: 'doc 6' } }, @@ -1515,13 +1521,34 @@ describe('migration actions', () => { refresh: 'wait_for', })() ).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "type": "target_index_had_write_block", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "type": "target_index_had_write_block", + }, + } + `); + }); + it('resolves left request_entity_too_large_exception when the payload is too large', async () => { + const newDocs = new Array(10000).fill({ + _source: { + title: + 'how do I create a document thats large enoug to exceed the limits without typing long sentences', + }, + }) as SavedObjectsRawDoc[]; + const task = bulkOverwriteTransformedDocuments({ + client, + index: 'existing_index_with_docs', + transformedDocs: newDocs, + }); + await expect(task()).resolves.toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "type": "request_entity_too_large_exception", + }, + } + `); }); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index d4862cddf2666..773a0af469bd4 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -353,6 +353,9 @@ describe('migrationsStateActionMachine', () => { next: () => { throw new ResponseError( elasticsearchClientMock.createApiResponse({ + meta: { + request: { options: {}, id: '', params: { method: 'POST', path: '/mock' } }, + } as any, body: { error: { type: 'snapshot_in_progress_exception', @@ -365,14 +368,14 @@ describe('migrationsStateActionMachine', () => { client: esClient, }) ).rejects.toMatchInlineSnapshot( - `[Error: Unable to complete saved object migrations for the [.my-so-index] index. Please check the health of your Elasticsearch cluster and try again. Error: [snapshot_in_progress_exception]: Cannot delete indices that are being snapshotted]` + `[Error: Unable to complete saved object migrations for the [.my-so-index] index. Please check the health of your Elasticsearch cluster and try again. Unexpected Elasticsearch ResponseError: statusCode: 200, method: POST, url: /mock error: [snapshot_in_progress_exception]: Cannot delete indices that are being snapshotted,]` ); expect(loggingSystemMock.collect(mockLogger)).toMatchInlineSnapshot(` Object { "debug": Array [], "error": Array [ Array [ - "[.my-so-index] [snapshot_in_progress_exception]: Cannot delete indices that are being snapshotted", + "[.my-so-index] Unexpected Elasticsearch ResponseError: statusCode: 200, method: POST, url: /mock error: [snapshot_in_progress_exception]: Cannot delete indices that are being snapshotted,", ], Array [ "[.my-so-index] migration failed, dumping execution log:", diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index cd42d4077695e..8e3b8ee4ab556 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -10,6 +10,7 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; import * as Option from 'fp-ts/lib/Option'; import { Logger, LogMeta } from '../../logging'; import type { ElasticsearchClient } from '../../elasticsearch'; +import { getErrorMessage, getRequestDebugMeta } from '../../elasticsearch'; import { Model, Next, stateActionMachine } from './state_action_machine'; import { cleanup } from './migrations_state_machine_cleanup'; import { State } from './types'; @@ -196,16 +197,19 @@ export async function migrationStateActionMachine({ } catch (e) { await cleanup(client, executionLog, lastState); if (e instanceof EsErrors.ResponseError) { - logger.error( - logMessagePrefix + `[${e.body?.error?.type}]: ${e.body?.error?.reason ?? e.message}` - ); + // Log the failed request. This is very similar to the + // elasticsearch-service's debug logs, but we log everything in single + // line until we have sub-ms resolution in our cloud logs. Because this + // is error level logs, we're also more careful and don't log the request + // body since this can very likely have sensitive saved objects. + const req = getRequestDebugMeta(e.meta); + const failedRequestMessage = `Unexpected Elasticsearch ResponseError: statusCode: ${ + req.statusCode + }, method: ${req.method}, url: ${req.url} error: ${getErrorMessage(e)},`; + logger.error(logMessagePrefix + failedRequestMessage); dumpExecutionLog(logger, logMessagePrefix, executionLog); throw new Error( - `Unable to complete saved object migrations for the [${ - initialState.indexPrefix - }] index. Please check the health of your Elasticsearch cluster and try again. Error: [${ - e.body?.error?.type - }]: ${e.body?.error?.reason ?? e.message}` + `Unable to complete saved object migrations for the [${initialState.indexPrefix}] index. Please check the health of your Elasticsearch cluster and try again. ${failedRequestMessage}` ); } else { logger.error(e); diff --git a/src/core/server/saved_objects/migrationsv2/model/model.test.ts b/src/core/server/saved_objects/migrationsv2/model/model.test.ts index 0837183b2a116..30612b82d58aa 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.test.ts @@ -1154,6 +1154,16 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK -> FATAL if action returns left request_entity_too_large_exception', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({ + type: 'request_entity_too_large_exception', + }); + const newState = model(reindexSourceToTempIndexBulkState, res) as FatalState; + expect(newState.controlState).toEqual('FATAL'); + expect(newState.reason).toMatchInlineSnapshot( + `"While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Try to use smaller batches by changing the Kibana 'migrations.batchSize' configuration option and restarting Kibana."` + ); + }); test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK should throw a throwBadResponse error if action failed', () => { const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'> = Either.left({ type: 'retryable_es_client_error', @@ -1532,6 +1542,17 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(1); expect(newState.retryDelay).toEqual(2000); }); + + test('TRANSFORMED_DOCUMENTS_BULK_INDEX -> FATAL if action returns left request_entity_too_large_exception', () => { + const res: ResponseType<'TRANSFORMED_DOCUMENTS_BULK_INDEX'> = Either.left({ + type: 'request_entity_too_large_exception', + }); + const newState = model(transformedDocumentsBulkIndexState, res) as FatalState; + expect(newState.controlState).toEqual('FATAL'); + expect(newState.reason).toMatchInlineSnapshot( + `"While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Try to use smaller batches by changing the Kibana 'migrations.batchSize' configuration option and restarting Kibana."` + ); + }); }); describe('UPDATE_TARGET_MAPPINGS', () => { diff --git a/src/core/server/saved_objects/migrationsv2/model/model.ts b/src/core/server/saved_objects/migrationsv2/model/model.ts index 474fe56ed64a4..01c1893154c6c 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.ts @@ -540,6 +540,12 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_CLOSE_PIT', }; + } else if (isLeftTypeof(res.left, 'request_entity_too_large_exception')) { + return { + ...stateP, + controlState: 'FATAL', + reason: `While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Try to use smaller batches by changing the Kibana 'migrations.batchSize' configuration option and restarting Kibana.`, + }; } throwBadResponse(stateP, res.left); } @@ -709,7 +715,19 @@ export const model = (currentState: State, resW: ResponseType): hasTransformedDocs: true, }; } else { - throwBadResponse(stateP, res as never); + if (isLeftTypeof(res.left, 'request_entity_too_large_exception')) { + return { + ...stateP, + controlState: 'FATAL', + reason: `While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Try to use smaller batches by changing the Kibana 'migrations.batchSize' configuration option and restarting Kibana.`, + }; + } else if (isLeftTypeof(res.left, 'target_index_had_write_block')) { + // we fail on this error since the target index will only have a write + // block if a newer version of Kibana started an upgrade + throwBadResponse(stateP, res.left as never); + } else { + throwBadResponse(stateP, res.left); + } } } else if (stateP.controlState === 'UPDATE_TARGET_MAPPINGS') { const res = resW as ExcludeRetryableEsError>; From 3b4dca1efbe8f794b842e16f2d240b54fab63025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 13 Aug 2021 06:38:54 -0400 Subject: [PATCH 40/91] [APM] Backends UI: Show "NEW" badge in the Observability solution nav for the new Backends view (#108397) * adding badge to obs nav * addressing PR comments * refacroting --- x-pack/plugins/apm/public/plugin.ts | 36 ++++++----- .../page_template/nav_name_with_badge.tsx | 62 +++++++++++++++++++ .../shared/page_template/page_template.tsx | 18 +++++- .../public/services/navigation_registry.ts | 2 + 4 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/page_template/nav_name_with_badge.tsx diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 0631dba7e2a34..32eed1cf53d80 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -81,6 +81,24 @@ export interface ApmPluginStartDeps { fleet?: FleetStart; } +const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { + defaultMessage: 'Services', +}); +const tracesTitle = i18n.translate('xpack.apm.navigation.tracesTitle', { + defaultMessage: 'Traces', +}); +const serviceMapTitle = i18n.translate('xpack.apm.navigation.serviceMapTitle', { + defaultMessage: 'Service Map', +}); + +const backendsTitle = i18n.translate('xpack.apm.navigation.backendsTitle', { + defaultMessage: 'Backends', +}); + +const newBadgeLabel = i18n.translate('xpack.apm.navigation.newBadge', { + defaultMessage: 'NEW', +}); + export class ApmPlugin implements Plugin { constructor( private readonly initializerContext: PluginInitializerContext @@ -96,21 +114,6 @@ export class ApmPlugin implements Plugin { pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); } - const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { - defaultMessage: 'Services', - }); - const tracesTitle = i18n.translate('xpack.apm.navigation.tracesTitle', { - defaultMessage: 'Traces', - }); - const serviceMapTitle = i18n.translate( - 'xpack.apm.navigation.serviceMapTitle', - { defaultMessage: 'Service Map' } - ); - - const backendsTitle = i18n.translate('xpack.apm.navigation.backendsTitle', { - defaultMessage: 'Backends', - }); - // register observability nav if user has access to plugin plugins.observability.navigation.registerSections( from(core.getStartServices()).pipe( @@ -124,11 +127,11 @@ export class ApmPlugin implements Plugin { entries: [ { label: servicesTitle, app: 'apm', path: '/services' }, { label: tracesTitle, app: 'apm', path: '/traces' }, - { label: serviceMapTitle, app: 'apm', path: '/service-map' }, { label: backendsTitle, app: 'apm', path: '/backends', + sideBadgeLabel: newBadgeLabel, onClick: () => { const { usageCollection } = pluginsStart as { usageCollection?: UsageCollectionStart; @@ -143,6 +146,7 @@ export class ApmPlugin implements Plugin { } }, }, + { label: serviceMapTitle, app: 'apm', path: '/service-map' }, ], }, diff --git a/x-pack/plugins/observability/public/components/shared/page_template/nav_name_with_badge.tsx b/x-pack/plugins/observability/public/components/shared/page_template/nav_name_with_badge.tsx new file mode 100644 index 0000000000000..fc3d249dac9f8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/page_template/nav_name_with_badge.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiBadge } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +interface Props { + label: string; + badgeLabel: string; + localstorageId: string; +} + +const LabelContainer = styled.span` + max-width: 72%; + float: left; + &:hover, + &:focus { + text-decoration: underline; + } +`; + +const StyledBadge = styled(EuiBadge)` + margin-left: 8px; +`; + +/** + * Gets current state from local storage to show or hide the badge. + * Default value: true + * @param localstorageId + */ +function getBadgeVisibility(localstorageId: string) { + const storedItem = window.localStorage.getItem(localstorageId); + if (storedItem) { + return JSON.parse(storedItem) as boolean; + } + + return true; +} + +/** + * Saves on local storage that this item should no longer be visible + * @param localstorageId + */ +export function hideBadge(localstorageId: string) { + window.localStorage.setItem(localstorageId, JSON.stringify(false)); +} + +export function NavNameWithBadge({ label, badgeLabel, localstorageId }: Props) { + const isBadgeVisible = getBadgeVisibility(localstorageId); + return ( + <> + + {label} + + {isBadgeVisible && {badgeLabel}} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx index 61feba83431f5..5a9b07a15d714 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx @@ -17,6 +17,7 @@ import { KibanaPageTemplateProps, } from '../../../../../../../src/plugins/kibana_react/public'; import type { NavigationSection } from '../../../services/navigation_registry'; +import { NavNameWithBadge, hideBadge } from './nav_name_with_badge'; export type WrappedPageTemplateProps = Pick< KibanaPageTemplateProps, @@ -71,10 +72,18 @@ export function ObservabilityPageTemplate({ exact: !!entry.matchFullPath, strict: !entry.ignoreTrailingSlash, }) != null; - + const badgeLocalStorageId = `observability.nav_item_badge_visible_${entry.app}${entry.path}`; return { id: `${sectionIndex}.${entryIndex}`, - name: entry.label, + name: entry.sideBadgeLabel ? ( + + ) : ( + entry.label + ), href, isSelected, onClick: (event) => { @@ -82,6 +91,11 @@ export function ObservabilityPageTemplate({ entry.onClick(event); } + // When side badge is defined hides it when the item is clicked + if (entry.sideBadgeLabel) { + hideBadge(badgeLocalStorageId); + } + if ( event.button !== 0 || event.defaultPrevented || diff --git a/x-pack/plugins/observability/public/services/navigation_registry.ts b/x-pack/plugins/observability/public/services/navigation_registry.ts index 4789e4c5ea574..7e0c8b304c07f 100644 --- a/x-pack/plugins/observability/public/services/navigation_registry.ts +++ b/x-pack/plugins/observability/public/services/navigation_registry.ts @@ -30,6 +30,8 @@ export interface NavigationEntry { ignoreTrailingSlash?: boolean; // handler to be called when the item is clicked onClick?: (event: React.MouseEvent) => void; + // the label of the badge that is shown besides the navigation label + sideBadgeLabel?: string; } export interface NavigationRegistry { From 444355cdc334a2aed719988422043483bc712952 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 13 Aug 2021 13:05:45 +0200 Subject: [PATCH 41/91] [APM] Index reason field for alerts (#108019) --- x-pack/plugins/apm/common/alert_types.ts | 84 +++++++++++++++++++ .../alerting/register_apm_alerts.ts | 69 +++++++-------- .../alerts/register_error_count_alert_type.ts | 9 ++ ...egister_transaction_duration_alert_type.ts | 11 +++ ...transaction_duration_anomaly_alert_type.ts | 9 ++ ...ister_transaction_error_rate_alert_type.ts | 11 +++ x-pack/plugins/observability/common/index.ts | 2 + .../tests/alerts/rule_registry.ts | 6 ++ 8 files changed, 160 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index fa706b7d8cb35..68ca22c41ec92 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import type { ValuesType } from 'utility-types'; +import type { AsDuration, AsPercent } from '../../observability/common'; import type { ActionGroup } from '../../alerting/common'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './ml_constants'; @@ -28,6 +29,89 @@ const THRESHOLD_MET_GROUP: ActionGroup = { }), }; +export function formatErrorCountReason({ + threshold, + measured, + serviceName, +}: { + threshold: number; + measured: number; + serviceName: string; +}) { + return i18n.translate('xpack.apm.alertTypes.errorCount.reason', { + defaultMessage: `Error count is greater than {threshold} (current value is {measured}) for {serviceName}`, + values: { + threshold, + measured, + serviceName, + }, + }); +} + +export function formatTransactionDurationReason({ + threshold, + measured, + serviceName, + asDuration, +}: { + threshold: number; + measured: number; + serviceName: string; + asDuration: AsDuration; +}) { + return i18n.translate('xpack.apm.alertTypes.transactionDuration.reason', { + defaultMessage: `Latency is above {threshold} (current value is {measured}) for {serviceName}`, + values: { + threshold: asDuration(threshold), + measured: asDuration(measured), + serviceName, + }, + }); +} + +export function formatTransactionErrorRateReason({ + threshold, + measured, + serviceName, + asPercent, +}: { + threshold: number; + measured: number; + serviceName: string; + asPercent: AsPercent; +}) { + return i18n.translate('xpack.apm.alertTypes.transactionErrorRate.reason', { + defaultMessage: `Failed transactions rate is greater than {threshold} (current value is {measured}) for {serviceName}`, + values: { + threshold: asPercent(threshold, 100), + measured: asPercent(measured, 100), + serviceName, + }, + }); +} + +export function formatTransactionDurationAnomalyReason({ + serviceName, + severityLevel, + measured, +}: { + serviceName: string; + severityLevel: string; + measured: number; +}) { + return i18n.translate( + 'xpack.apm.alertTypes.transactionDurationAnomaly.reason', + { + defaultMessage: `{severityLevel} anomaly detected for {serviceName} (score was {measured})`, + values: { + serviceName, + severityLevel, + measured, + }, + } + ); +} + export const ALERT_TYPES_CONFIG: Record< AlertType, { diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 12102a294bf9f..5905f700b0bbe 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -21,7 +21,13 @@ import { } from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; -import { AlertType } from '../../../common/alert_types'; +import { + AlertType, + formatErrorCountReason, + formatTransactionDurationAnomalyReason, + formatTransactionDurationReason, + formatTransactionErrorRateReason, +} from '../../../common/alert_types'; // copied from elasticsearch_fieldnames.ts to limit page load bundle size const SERVICE_ENVIRONMENT = 'service.environment'; @@ -53,13 +59,10 @@ export function registerApmAlerts( }), format: ({ fields }) => { return { - reason: i18n.translate('xpack.apm.alertTypes.errorCount.reason', { - defaultMessage: `Error count is greater than {threshold} (current value is {measured}) for {serviceName}`, - values: { - threshold: fields[ALERT_EVALUATION_THRESHOLD], - measured: fields[ALERT_EVALUATION_VALUE], - serviceName: String(fields[SERVICE_NAME][0]), - }, + reason: formatErrorCountReason({ + threshold: fields[ALERT_EVALUATION_THRESHOLD]!, + measured: fields[ALERT_EVALUATION_VALUE]!, + serviceName: String(fields[SERVICE_NAME][0]), }), link: format({ pathname: `/app/apm/services/${String( @@ -105,17 +108,12 @@ export function registerApmAlerts( } ), format: ({ fields, formatters: { asDuration } }) => ({ - reason: i18n.translate( - 'xpack.apm.alertTypes.transactionDuration.reason', - { - defaultMessage: `Latency is above {threshold} (current value is {measured}) for {serviceName}`, - values: { - threshold: asDuration(fields[ALERT_EVALUATION_THRESHOLD]), - measured: asDuration(fields[ALERT_EVALUATION_VALUE]), - serviceName: String(fields[SERVICE_NAME][0]), - }, - } - ), + reason: formatTransactionDurationReason({ + threshold: fields[ALERT_EVALUATION_THRESHOLD]!, + measured: fields[ALERT_EVALUATION_VALUE]!, + serviceName: String(fields[SERVICE_NAME][0]), + asDuration, + }), link: format({ pathname: `/app/apm/services/${fields[SERVICE_NAME][0]!}`, query: { @@ -161,17 +159,12 @@ export function registerApmAlerts( } ), format: ({ fields, formatters: { asPercent } }) => ({ - reason: i18n.translate( - 'xpack.apm.alertTypes.transactionErrorRate.reason', - { - defaultMessage: `Failed transactions rate is greater than {threshold} (current value is {measured}) for {serviceName}`, - values: { - threshold: asPercent(fields[ALERT_EVALUATION_THRESHOLD], 100), - measured: asPercent(fields[ALERT_EVALUATION_VALUE], 100), - serviceName: String(fields[SERVICE_NAME][0]), - }, - } - ), + reason: formatTransactionErrorRateReason({ + threshold: fields[ALERT_EVALUATION_THRESHOLD]!, + measured: fields[ALERT_EVALUATION_VALUE]!, + serviceName: String(fields[SERVICE_NAME][0]), + asPercent, + }), link: format({ pathname: `/app/apm/services/${String(fields[SERVICE_NAME][0]!)}`, query: { @@ -216,17 +209,11 @@ export function registerApmAlerts( } ), format: ({ fields }) => ({ - reason: i18n.translate( - 'xpack.apm.alertTypes.transactionDurationAnomaly.reason', - { - defaultMessage: `{severityLevel} anomaly detected for {serviceName} (score was {measured})`, - values: { - serviceName: String(fields[SERVICE_NAME][0]), - severityLevel: String(fields[ALERT_SEVERITY_LEVEL]), - measured: Number(fields[ALERT_EVALUATION_VALUE]), - }, - } - ), + reason: formatTransactionDurationAnomalyReason({ + serviceName: String(fields[SERVICE_NAME][0]), + severityLevel: String(fields[ALERT_SEVERITY_LEVEL]), + measured: Number(fields[ALERT_EVALUATION_VALUE]), + }), link: format({ pathname: `/app/apm/services/${String(fields[SERVICE_NAME][0])}`, query: { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 6a6a67e9fd97f..c78c24fba8673 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -10,10 +10,12 @@ import { take } from 'rxjs/operators'; import type { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_REASON as ALERT_REASON_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; @@ -26,6 +28,7 @@ import { AlertType, APM_SERVER_FEATURE_ID, ALERT_TYPES_CONFIG, + formatErrorCountReason, } from '../../../common/alert_types'; import { PROCESSOR_EVENT, @@ -41,6 +44,7 @@ import { RegisterRuleDependencies } from './register_apm_alerts'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; const paramsSchema = schema.object({ windowSize: schema.number(), @@ -158,6 +162,11 @@ export function registerErrorCountAlertType({ [PROCESSOR_EVENT]: ProcessorEvent.error, [ALERT_EVALUATION_VALUE]: errorCount, [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, + [ALERT_REASON]: formatErrorCountReason({ + serviceName, + threshold: alertParams.threshold, + measured: errorCount, + }), }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index 790e62eae66d4..553b791e757b2 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -11,12 +11,15 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import type { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_REASON as ALERT_REASON_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; +import { asDuration } from '../../../../observability/common/utils/formatters'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { getEnvironmentLabel, @@ -26,6 +29,7 @@ import { AlertType, APM_SERVER_FEATURE_ID, ALERT_TYPES_CONFIG, + formatTransactionDurationReason, } from '../../../common/alert_types'; import { PROCESSOR_EVENT, @@ -43,6 +47,7 @@ import { RegisterRuleDependencies } from './register_apm_alerts'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; const paramsSchema = schema.object({ serviceName: schema.string(), @@ -178,6 +183,12 @@ export function registerTransactionDurationAlertType({ [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: transactionDuration, [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, + [ALERT_REASON]: formatTransactionDurationReason({ + measured: transactionDuration, + serviceName: alertParams.serviceName, + threshold: alertParams.threshold, + asDuration, + }), }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 2041f06a5a6a8..e38262773b6db 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -14,12 +14,14 @@ import type { ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_TYPED, + ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_NON_TYPED, + ALERT_REASON as ALERT_REASON_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; @@ -37,6 +39,7 @@ import { AlertType, ALERT_TYPES_CONFIG, ANOMALY_ALERT_SEVERITY_TYPES, + formatTransactionDurationAnomalyReason, } from '../../../common/alert_types'; import { getMLJobs } from '../service_map/get_service_anomalies'; import { apmActionVariables } from './action_variables'; @@ -50,6 +53,7 @@ const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALER const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; const ALERT_SEVERITY_VALUE: typeof ALERT_SEVERITY_VALUE_TYPED = ALERT_SEVERITY_VALUE_NON_TYPED; +const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; const paramsSchema = schema.object({ serviceName: schema.maybe(schema.string()), @@ -258,6 +262,11 @@ export function registerTransactionDurationAnomalyAlertType({ [ALERT_SEVERITY_VALUE]: score, [ALERT_EVALUATION_VALUE]: score, [ALERT_EVALUATION_THRESHOLD]: threshold, + [ALERT_REASON]: formatTransactionDurationAnomalyReason({ + measured: score, + serviceName, + severityLevel, + }), }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index fc63e4827e5de..17061cdacd51e 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -10,10 +10,12 @@ import { take } from 'rxjs/operators'; import type { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_REASON as ALERT_REASON_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { @@ -26,6 +28,7 @@ import { AlertType, ALERT_TYPES_CONFIG, APM_SERVER_FEATURE_ID, + formatTransactionErrorRateReason, } from '../../../common/alert_types'; import { EVENT_OUTCOME, @@ -42,9 +45,11 @@ import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +import { asPercent } from '../../../../observability/common/utils/formatters'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; const paramsSchema = schema.object({ windowSize: schema.number(), @@ -217,6 +222,12 @@ export function registerTransactionErrorRateAlertType({ [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: errorRate, [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, + [ALERT_REASON]: formatTransactionErrorRateReason({ + threshold: alertParams.threshold, + measured: errorRate, + asPercent, + serviceName, + }), }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 114379719781a..baa3d92ffeeb3 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +export type { AsDuration, AsPercent } from './utils/formatters'; + export const casesFeatureId = 'observabilityCases'; // The ID of the observability app. Should more appropriately be called diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 00385e4ff3028..857101ebcc18c 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -369,6 +369,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], + "kibana.alert.reason": Array [ + "Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go", + ], "kibana.alert.rule.category": Array [ "Failed transaction rate threshold", ], @@ -473,6 +476,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], + "kibana.alert.reason": Array [ + "Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go", + ], "kibana.alert.rule.category": Array [ "Failed transaction rate threshold", ], From 6a7cb09764f3cdadbd512cc04b314855e0258160 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 13 Aug 2021 13:26:53 +0200 Subject: [PATCH 42/91] [Dashboard] Minor copy tweak for copy to dashboard action --- src/plugins/dashboard/public/dashboard_strings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index 34a3317289f11..a32acf8d3bdf7 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -92,7 +92,7 @@ export const dashboardCopyToDashboardAction = { }), getDescription: () => i18n.translate('dashboard.panel.copyToDashboard.description', { - defaultMessage: "Select where to copy the panel. You're navigated to destination dashboard.", + defaultMessage: 'Choose the destination dashboard.', }), }; From 7fb30ba6f41ee2d1cfd264b08a4f8cde34d9f73e Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 13 Aug 2021 05:22:52 -0700 Subject: [PATCH 43/91] [Fleet] Link to Add Data (Beats tutorials) when there are no integrations found (#108224) * Link to add data/beats tutorials when there are no integrations found * Address PR feedback + fox i18n * Update integrations page subtitle Co-authored-by: Kyle Pollich --- .../applications/integrations/layouts/default.tsx | 6 +++--- .../sections/epm/components/package_list_grid.tsx | 14 +++++++++++++- .../plugins/translations/translations/ja-JP.json | 1 - .../plugins/translations/translations/zh-CN.json | 1 - 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx index 66b88c020f163..b20c275cdeddf 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx @@ -61,8 +61,8 @@ export const DefaultLayout: React.FunctionComponent = memo(({ section, ch

@@ -74,7 +74,7 @@ export const DefaultLayout: React.FunctionComponent = memo(({ section, ch

diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx index 7e56c25c2bbe7..e017de9206375 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx @@ -22,6 +22,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useStartServices } from '../../../../../hooks'; import { Loading } from '../../../components'; import type { PackageList } from '../../../types'; import { useLocalSearch, searchIdField } from '../../../hooks'; @@ -191,6 +192,9 @@ function MissingIntegrationContent({ resetQuery, setSelectedCategory, }: MissingIntegrationContentProps) { + const { + application: { getUrlForApp }, + } = useStartServices(); const handleCustomInputsLinkClick = useCallback(() => { resetQuery(); setSelectedCategory('custom'); @@ -201,7 +205,7 @@ function MissingIntegrationContent({

@@ -219,6 +223,14 @@ function MissingIntegrationContent({ /> ), + beatsTutorialLink: ( + + + + ), }} />

diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d1ce8e1e42965..9d880276312f7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9831,7 +9831,6 @@ "xpack.fleet.integrations.discussForumLink": "ディスカッションフォーラム", "xpack.fleet.integrations.installPackage.installingPackageButtonLabel": "{title} アセットをインストールしています", "xpack.fleet.integrations.installPackage.installPackageButtonLabel": "{title}アセットをインストール", - "xpack.fleet.integrations.missing": "統合が表示されない場合{customInputsLink}を使用してログまたはメトリックを収集してください。{discussForumLink}を使用して新しい統合を要求してください。", "xpack.fleet.integrations.packageInstallErrorDescription": "このパッケージのインストール中に問題が発生しました。しばらくたってから再試行してください。", "xpack.fleet.integrations.packageInstallErrorTitle": "{title}パッケージをインストールできませんでした", "xpack.fleet.integrations.packageInstallSuccessDescription": "正常に{title}をインストールしました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 43b1f23233db1..addbe289ee990 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10093,7 +10093,6 @@ "xpack.fleet.integrations.discussForumLink": "讨论论坛", "xpack.fleet.integrations.installPackage.installingPackageButtonLabel": "正在安装 {title} 资产", "xpack.fleet.integrations.installPackage.installPackageButtonLabel": "安装 {title} 资产", - "xpack.fleet.integrations.missing": "未看到集成?使用我们的{customInputsLink}收集任何日志或指标。使用{discussForumLink}请求新的集成。", "xpack.fleet.integrations.packageInstallErrorDescription": "尝试安装此软件包时出现问题。请稍后重试。", "xpack.fleet.integrations.packageInstallErrorTitle": "无法安装 {title} 软件包", "xpack.fleet.integrations.packageInstallSuccessDescription": "已成功安装 {title}", From 37053e6a8d8cf04f662da4d2f40ddf899c4cefb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 13 Aug 2021 14:37:50 +0200 Subject: [PATCH 44/91] [Security solution][Endpoint] Don't hide "add trusted app" button if we are checking if data exists and there was data before (#108373) * Don't hide add button if we are checking if data exists and there was data before * Moves duplicated code in a useCallback --- .../pages/trusted_apps/view/trusted_apps_page.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index de4384b2fac48..a7e86a6703bb5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -87,6 +87,11 @@ export const TrustedAppsPage = memo(() => { const showCreateFlyout = !!location.show; + const canDisplayContent = useCallback( + () => doEntriesExist || (isCheckingIfEntriesExists && didEntriesExist), + [didEntriesExist, doEntriesExist, isCheckingIfEntriesExists] + ); + const backButton = useMemo(() => { if (routeState && routeState.onBackButtonNavigateTo) { return ; @@ -121,7 +126,7 @@ export const TrustedAppsPage = memo(() => { /> )} - {doEntriesExist || (isCheckingIfEntriesExists && didEntriesExist) ? ( + {canDisplayContent() ? ( <> { } headerBackComponent={backButton} subtitle={ABOUT_TRUSTED_APPS} - actions={doEntriesExist ? addButton : <>} + actions={canDisplayContent() ? addButton : <>} > From dcfb19fb53b42798bd1c807796892f6e55490c76 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Fri, 13 Aug 2021 08:51:52 -0400 Subject: [PATCH 45/91] Reposition the take action popover on scroll (#108475) --- .../public/detections/components/take_action_dropdown/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index b49440c39203f..bd0ec4e2e742e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -266,6 +266,7 @@ export const TakeActionDropdown = React.memo( closePopover={closePopoverHandler} panelPaddingSize="none" anchorPosition="downLeft" + repositionOnScroll > From 3c75b1faf5bb9cd1ca7909491b8e009046d4cff4 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 13 Aug 2021 14:15:45 +0100 Subject: [PATCH 46/91] skip flaky suite (#106660) --- x-pack/test/functional/apps/infra/home_page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index e344f86f89fe8..90b34702767e4 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -87,7 +87,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('Saved Views', () => { + // FLAKY: https://github.com/elastic/kibana/issues/106660 + describe.skip('Saved Views', () => { before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); it('should have save and load controls', async () => { From a474a63a7f6bd3625a9292a5b40a751984f2bb2d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 13 Aug 2021 15:50:14 +0200 Subject: [PATCH 47/91] [APM] Make environment & kuery required (#108338) --- packages/kbn-io-ts-utils/src/index.ts | 1 + .../src/non_empty_string_rt/index.test.ts | 19 ++++ .../src/non_empty_string_rt/index.ts | 22 +++++ .../src/create_router.test.tsx | 38 +++++++- .../src/create_router.ts | 13 ++- .../src/types/index.ts | 16 ++-- .../apm/common/environment_filter_values.ts | 9 +- x-pack/plugins/apm/common/environment_rt.ts | 22 +++++ .../search_strategies/correlations/types.ts | 4 +- .../common/utils/environment_query.test.ts | 4 +- .../apm/common/utils/environment_query.ts | 2 +- .../integration/read_only_user/home.spec.ts | 2 +- .../app/RumDashboard/LocalUIFilters/index.tsx | 3 +- .../RumDashboard/LocalUIFilters/queries.ts | 2 +- .../get_redirect_to_trace_page_url.ts | 2 +- .../app/TraceLink/trace_link.test.tsx | 2 +- .../backend_detail_dependencies_table.tsx | 5 +- .../backend_error_rate_chart.tsx | 7 +- .../backend_latency_chart.tsx | 7 +- .../backend_throughput_chart.tsx | 7 +- .../app/backend_detail_overview/index.tsx | 8 +- .../index.tsx | 5 +- .../app/correlations/error_correlations.tsx | 14 ++- .../components/app/correlations/index.tsx | 37 ++++--- .../app/correlations/latency_correlations.tsx | 15 ++- .../app/error_group_details/index.tsx | 8 +- .../app/error_group_overview/index.tsx | 2 + .../app/service_inventory/index.tsx | 29 +++--- .../service_list/service_list.test.tsx | 3 + .../components/app/service_logs/index.tsx | 8 +- .../service_map/Popover/Popover.stories.tsx | 11 ++- .../service_map/Popover/backend_contents.tsx | 4 +- .../app/service_map/Popover/index.tsx | 13 ++- .../service_map/Popover/service_contents.tsx | 16 ++-- .../components/app/service_map/index.test.tsx | 28 ++++-- .../components/app/service_map/index.tsx | 35 ++++++- .../components/app/service_metrics/index.tsx | 7 ++ .../app/service_node_metrics/index.tsx | 10 +- .../app/service_node_overview/index.tsx | 10 +- .../components/app/service_overview/index.tsx | 35 +++++-- .../index.tsx | 3 +- .../service_overview_errors_table/index.tsx | 14 ++- ...ice_overview_instances_chart_and_table.tsx | 7 +- .../service_overview_throughput_chart.tsx | 13 +-- .../app/service_profiling/index.tsx | 4 +- .../service_profiling_flamegraph.tsx | 4 +- .../components/app/trace_overview/index.tsx | 11 ++- .../app/transaction_details/index.tsx | 11 ++- .../MaybeViewTraceLink.tsx | 5 +- .../waterfall_with_summary/index.tsx | 6 ++ .../span_flyout/sticky_span_properties.tsx | 5 +- .../Waterfall/waterfall_item.tsx | 4 +- .../app/transaction_overview/index.tsx | 13 ++- .../public/components/routing/home/index.tsx | 14 +-- .../routing/service_detail/index.tsx | 12 ++- .../shared/EnvironmentFilter/index.tsx | 5 +- .../shared/charts/latency_chart/index.tsx | 9 +- .../latency_chart/latency_chart.stories.tsx | 9 +- .../transaction_breakdown_chart/index.tsx | 6 +- .../use_transaction_breakdown.ts | 10 +- .../charts/transaction_charts/index.tsx | 27 ++++-- .../transaction_error_rate_chart/index.tsx | 6 +- .../shared/kuery_bar/get_bool_filter.ts | 5 +- .../components/shared/kuery_bar/index.tsx | 9 +- .../components/shared/kuery_bar/utils.ts | 2 +- .../shared/transactions_table/index.tsx | 6 +- .../annotations/annotations_context.tsx | 11 +-- .../apm_service/apm_service_context.tsx | 7 +- .../use_service_alerts_fetcher.tsx | 6 +- .../apm/public/hooks/use_apm_params.ts | 8 +- .../use_error_group_distribution_fetcher.tsx | 6 +- .../use_fallback_to_transactions_fetcher.tsx | 4 +- .../use_service_metric_charts_fetcher.ts | 6 +- .../use_transaction_distribution_fetcher.ts | 6 +- .../use_transaction_latency_chart_fetcher.ts | 10 +- .../apm/scripts/optimize-tsconfig/optimize.js | 1 + .../get_error_rate_charts_for_backend.ts | 4 +- .../get_latency_charts_for_backend.ts | 4 +- .../get_throughput_charts_for_backend.ts | 4 +- .../server/lib/backends/get_top_backends.ts | 4 +- .../get_upstream_services_for_backend.ts | 4 +- .../server/lib/correlations/get_filters.ts | 4 +- .../errors/distribution/get_buckets.test.ts | 1 + .../lib/errors/distribution/get_buckets.ts | 4 +- .../errors/distribution/get_distribution.ts | 4 +- .../lib/errors/distribution/queries.test.ts | 5 + .../lib/errors/get_error_group_sample.ts | 4 +- .../apm/server/lib/errors/get_error_groups.ts | 4 +- .../apm/server/lib/errors/queries.test.ts | 7 ++ .../get_fallback_to_transactions.ts | 2 +- .../helpers/aggregated_transactions/index.ts | 4 +- .../server/lib/metrics/by_agent/default.ts | 4 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 4 +- .../by_agent/java/gc/get_gc_rate_chart.ts | 4 +- .../by_agent/java/gc/get_gc_time_chart.ts | 4 +- .../by_agent/java/heap_memory/index.ts | 4 +- .../server/lib/metrics/by_agent/java/index.ts | 4 +- .../by_agent/java/non_heap_memory/index.ts | 4 +- .../by_agent/java/thread_count/index.ts | 4 +- .../lib/metrics/by_agent/shared/cpu/index.ts | 4 +- .../metrics/by_agent/shared/memory/index.ts | 4 +- .../metrics/fetch_and_transform_metrics.ts | 4 +- .../get_metrics_chart_data_by_agent.ts | 4 +- .../apm/server/lib/metrics/queries.test.ts | 41 +++++++- .../rum_client/ui_filters/get_es_filter.ts | 5 +- .../queries/get_query_with_params.test.ts | 6 ++ .../queries/get_request_base.test.ts | 5 + .../queries/query_correlation.test.ts | 3 + .../queries/query_field_candidates.test.ts | 3 + .../queries/query_field_value_pairs.test.ts | 3 + .../queries/query_fractions.test.ts | 3 + .../queries/query_histogram.test.ts | 3 + .../queries/query_histogram_interval.test.ts | 3 + .../query_histogram_range_steps.test.ts | 3 + .../query_histograms_generator.test.ts | 3 + .../queries/query_percentiles.test.ts | 3 + .../correlations/queries/query_ranges.test.ts | 3 + .../correlations/search_strategy.test.ts | 3 + .../lib/service_map/get_service_anomalies.ts | 8 +- .../server/lib/service_map/get_service_map.ts | 9 +- .../get_service_map_backend_node_info.ts | 2 +- .../get_service_map_service_node_info.test.ts | 2 + .../get_service_map_service_node_info.ts | 7 +- .../lib/service_map/get_trace_sample_ids.ts | 2 +- .../apm/server/lib/service_nodes/index.ts | 11 ++- .../server/lib/service_nodes/queries.test.ts | 10 +- .../get_derived_service_annotations.ts | 2 +- .../annotations/get_stored_annotations.ts | 2 +- .../server/lib/services/annotations/index.ts | 2 +- .../server/lib/services/get_service_alerts.ts | 2 +- .../lib/services/get_service_dependencies.ts | 2 +- .../get_service_dependencies_breakdown.ts | 4 +- ...service_error_group_detailed_statistics.ts | 8 +- ...get_service_error_group_main_statistics.ts | 4 +- .../get_service_error_groups/index.ts | 4 +- .../services/get_service_infrastructure.ts | 4 +- .../detailed_statistics.ts | 8 +- ...vice_instances_system_metric_statistics.ts | 4 +- ...ervice_instances_transaction_statistics.ts | 4 +- .../get_service_instances/main_statistics.ts | 4 +- .../lib/services/get_service_node_metadata.ts | 4 +- ...e_transaction_group_detailed_statistics.ts | 8 +- .../get_service_transaction_groups.ts | 4 +- .../get_services/get_health_statuses.ts | 2 +- .../get_service_transaction_stats.ts | 4 +- .../get_services_from_metric_documents.ts | 4 +- .../get_services/get_services_items.ts | 4 +- .../server/lib/services/get_services/index.ts | 4 +- ...service_transaction_detailed_statistics.ts | 4 +- .../get_services_detailed_statistics/index.ts | 4 +- .../apm/server/lib/services/get_throughput.ts | 4 +- .../get_service_profiling_statistics.ts | 4 +- .../get_service_profiling_timeline.ts | 4 +- .../apm/server/lib/services/queries.test.ts | 3 + .../server/lib/transaction_groups/fetcher.ts | 4 +- .../lib/transaction_groups/get_error_rate.ts | 8 +- .../lib/transaction_groups/queries.test.ts | 5 + .../lib/transactions/breakdown/index.test.ts | 9 ++ .../lib/transactions/breakdown/index.ts | 4 +- .../distribution/get_buckets/index.ts | 4 +- .../distribution/get_distribution_max.ts | 4 +- .../lib/transactions/distribution/index.ts | 4 +- .../transactions/get_anomaly_data/index.ts | 2 +- .../transactions/get_latency_charts/index.ts | 12 +-- .../server/lib/transactions/queries.test.ts | 7 ++ .../plugins/apm/server/projections/errors.ts | 4 +- .../plugins/apm/server/projections/metrics.ts | 4 +- .../apm/server/projections/service_nodes.ts | 9 +- .../apm/server/projections/services.ts | 2 +- .../apm/server/routes/alerts/chart_preview.ts | 4 +- x-pack/plugins/apm/server/routes/backends.ts | 9 +- .../apm/server/routes/default_api_types.ts | 6 +- .../plugins/apm/server/routes/environments.ts | 10 +- .../server/routes/observability_overview.ts | 10 +- .../plugins/apm/server/routes/service_map.ts | 20 ++-- .../apm/server/routes/service_nodes.ts | 12 ++- x-pack/plugins/apm/server/routes/services.ts | 67 ++++++++----- .../routes/settings/agent_configuration.ts | 16 ++-- .../routes/settings/anomaly_detection.ts | 8 +- .../observability/server/utils/queries.ts | 2 +- .../tests/alerts/chart_preview.ts | 1 + .../errors_failed_transactions.ts | 2 + .../tests/correlations/errors_overall.ts | 2 + .../tests/correlations/latency_ml.ts | 1 + .../tests/correlations/latency_overall.ts | 2 + .../correlations/latency_slow_transactions.ts | 2 + .../tests/feature_controls.ts | 32 ++++--- .../tests/metrics_charts/metrics_charts.ts | 6 +- .../tests/service_maps/service_maps.ts | 22 ++++- .../service_overview/get_service_node_ids.ts | 2 + .../instances_detailed_statistics.ts | 6 ++ .../instances_main_statistics.ts | 8 ++ .../tests/services/annotations.ts | 8 +- .../error_groups_detailed_statistics.ts | 10 ++ .../services/error_groups_main_statistics.ts | 3 + .../tests/services/get_error_group_ids.ts | 2 + .../services/services_detailed_statistics.ts | 29 +++++- .../tests/services/throughput.ts | 7 ++ .../tests/services/top_services.ts | 20 ++-- .../tests/traces/top_traces.ts | 8 +- .../transactions/__snapshots__/breakdown.snap | 4 +- .../tests/transactions/breakdown.ts | 96 ++++++++++--------- .../tests/transactions/distribution.ts | 2 + .../tests/transactions/error_rate.ts | 8 +- .../tests/transactions/latency.ts | 11 +++ ...transactions_groups_detailed_statistics.ts | 10 ++ .../transactions_groups_main_statistics.ts | 6 ++ 207 files changed, 1164 insertions(+), 521 deletions(-) create mode 100644 packages/kbn-io-ts-utils/src/non_empty_string_rt/index.test.ts create mode 100644 packages/kbn-io-ts-utils/src/non_empty_string_rt/index.ts create mode 100644 x-pack/plugins/apm/common/environment_rt.ts diff --git a/packages/kbn-io-ts-utils/src/index.ts b/packages/kbn-io-ts-utils/src/index.ts index a60bc2086fa3a..e94ac76b3c27b 100644 --- a/packages/kbn-io-ts-utils/src/index.ts +++ b/packages/kbn-io-ts-utils/src/index.ts @@ -13,3 +13,4 @@ export { isoToEpochRt } from './iso_to_epoch_rt'; export { toNumberRt } from './to_number_rt'; export { toBooleanRt } from './to_boolean_rt'; export { toJsonSchema } from './to_json_schema'; +export { nonEmptyStringRt } from './non_empty_string_rt'; diff --git a/packages/kbn-io-ts-utils/src/non_empty_string_rt/index.test.ts b/packages/kbn-io-ts-utils/src/non_empty_string_rt/index.test.ts new file mode 100644 index 0000000000000..85b58ef76622f --- /dev/null +++ b/packages/kbn-io-ts-utils/src/non_empty_string_rt/index.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { nonEmptyStringRt } from './'; +import { isLeft, isRight } from 'fp-ts/lib/Either'; + +describe('nonEmptyStringRt', () => { + it('fails on empty strings', () => { + expect(isLeft(nonEmptyStringRt.decode(''))).toBe(true); + }); + + it('passes non-empty strings', () => { + expect(isRight(nonEmptyStringRt.decode('foo'))).toBe(true); + }); +}); diff --git a/packages/kbn-io-ts-utils/src/non_empty_string_rt/index.ts b/packages/kbn-io-ts-utils/src/non_empty_string_rt/index.ts new file mode 100644 index 0000000000000..740fcfe3f2f40 --- /dev/null +++ b/packages/kbn-io-ts-utils/src/non_empty_string_rt/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import * as t from 'io-ts'; + +// from https://github.com/gcanti/io-ts-types/blob/master/src/NonEmptyString.ts + +export interface NonEmptyStringBrand { + readonly NonEmptyString: unique symbol; +} + +export type NonEmptyString = t.Branded; + +export const nonEmptyStringRt = t.brand( + t.string, + (str): str is NonEmptyString => str.length > 0, + 'NonEmptyString' +); diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index 4de4b44196ddd..fe82c48a33332 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -61,6 +61,7 @@ describe('createRouter', () => { params: t.type({ query: t.type({ aggregationType: t.string, + kuery: t.string, }), }), }, @@ -112,7 +113,7 @@ describe('createRouter', () => { }, }); - history.push('/traces?rangeFrom=now-15m&rangeTo=now&aggregationType=avg'); + history.push('/traces?rangeFrom=now-15m&rangeTo=now&aggregationType=avg&kuery='); const topTracesParams = router.getParams('/traces', history.location); @@ -122,6 +123,7 @@ describe('createRouter', () => { rangeFrom: 'now-15m', rangeTo: 'now', aggregationType: 'avg', + kuery: '', }, }); @@ -156,6 +158,22 @@ describe('createRouter', () => { maxNumNodes: 3, }, }); + + history.push( + '/traces?rangeFrom=now-15m&rangeTo=now&aggregationType=avg&kuery=service.name%3A%22metricbeat%22' + ); + + const topTracesParams = router.getParams('/traces', history.location); + + expect(topTracesParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + aggregationType: 'avg', + kuery: 'service.name:"metricbeat"', + }, + }); }); it('throws an error if the given path does not match any routes', () => { @@ -280,10 +298,26 @@ describe('createRouter', () => { query: { rangeTo: 'now', aggregationType: 'avg', + kuery: '', + }, + }); + + expect(href).toEqual('/traces?aggregationType=avg&kuery=&rangeFrom=now-30m&rangeTo=now'); + }); + + it('encodes query parameters', () => { + const href = router.link('/traces', { + // @ts-ignore + query: { + rangeTo: 'now', + aggregationType: 'avg', + kuery: 'service.name:"metricbeat"', }, }); - expect(href).toEqual('/traces?aggregationType=avg&rangeFrom=now-30m&rangeTo=now'); + expect(href).toEqual( + '/traces?aggregationType=avg&kuery=service.name%3A%22metricbeat%22&rangeFrom=now-30m&rangeTo=now' + ); }); }); }); diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 846808cb798f1..5385eb44b747c 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -79,7 +79,7 @@ export function createRouter(routes: TRoutes): Router(routes: TRoutes): Router { params: TRoute extends { params: t.Type; } - ? t.OutputOf + ? t.TypeOf : {}; }; } @@ -107,9 +107,11 @@ type TypeOfMatches = TRouteMatches extends [ (TNextRouteMatches extends RouteMatch[] ? TypeOfMatches : {}) : {}; -export type TypeOf> = TypeOfMatches< - Match ->; +export type TypeOf< + TRoutes extends Route[], + TPath extends PathsOf, + TWithDefaultOutput extends boolean = true +> = TypeOfMatches> & (TWithDefaultOutput extends true ? DefaultOutput : {}); export type TypeAsArgs = keyof TObject extends never ? [] @@ -126,15 +128,15 @@ export interface Router { getParams>( path: TPath, location: Location - ): OutputOf; + ): TypeOf; getParams, TOptional extends boolean>( path: TPath, location: Location, optional: TOptional - ): TOptional extends true ? OutputOf | undefined : OutputOf; + ): TOptional extends true ? TypeOf | undefined : TypeOf; link>( path: TPath, - ...args: TypeAsArgs> + ...args: TypeAsArgs> ): string; getRoutePath(route: Route): string; } diff --git a/x-pack/plugins/apm/common/environment_filter_values.ts b/x-pack/plugins/apm/common/environment_filter_values.ts index fa5c580e89785..f0bd386f36de8 100644 --- a/x-pack/plugins/apm/common/environment_filter_values.ts +++ b/x-pack/plugins/apm/common/environment_filter_values.ts @@ -7,9 +7,10 @@ import { i18n } from '@kbn/i18n'; import { SERVICE_ENVIRONMENT } from './elasticsearch_fieldnames'; +import { Environment } from './environment_rt'; -const ENVIRONMENT_ALL_VALUE = 'ENVIRONMENT_ALL'; -const ENVIRONMENT_NOT_DEFINED_VALUE = 'ENVIRONMENT_NOT_DEFINED'; +const ENVIRONMENT_ALL_VALUE = 'ENVIRONMENT_ALL' as const; +const ENVIRONMENT_NOT_DEFINED_VALUE = 'ENVIRONMENT_NOT_DEFINED' as const; export function getEnvironmentLabel(environment: string) { if (!environment || environment === ENVIRONMENT_NOT_DEFINED_VALUE) { @@ -59,7 +60,7 @@ export function getNextEnvironmentUrlParam({ currentEnvironmentUrlParam, }: { requestedEnvironment?: string; - currentEnvironmentUrlParam?: string; + currentEnvironmentUrlParam: Environment; }) { const normalizedRequestedEnvironment = requestedEnvironment || ENVIRONMENT_NOT_DEFINED.value; @@ -67,7 +68,7 @@ export function getNextEnvironmentUrlParam({ currentEnvironmentUrlParam || ENVIRONMENT_ALL.value; if (normalizedRequestedEnvironment === normalizedQueryEnvironment) { - return currentEnvironmentUrlParam; + return currentEnvironmentUrlParam || ENVIRONMENT_ALL.value; } return ENVIRONMENT_ALL.value; diff --git a/x-pack/plugins/apm/common/environment_rt.ts b/x-pack/plugins/apm/common/environment_rt.ts new file mode 100644 index 0000000000000..e9337da9bdcf5 --- /dev/null +++ b/x-pack/plugins/apm/common/environment_rt.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import { + ENVIRONMENT_ALL, + ENVIRONMENT_NOT_DEFINED, +} from './environment_filter_values'; + +export const environmentRt = t.type({ + environment: t.union([ + t.literal(ENVIRONMENT_NOT_DEFINED.value), + t.literal(ENVIRONMENT_ALL.value), + nonEmptyStringRt, + ]), +}); + +export type Environment = t.TypeOf['environment']; diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts index bb697f0984335..70c1c7524cfe9 100644 --- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts @@ -19,8 +19,8 @@ export interface ResponseHit { } export interface SearchServiceParams { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName?: string; transactionName?: string; transactionType?: string; diff --git a/x-pack/plugins/apm/common/utils/environment_query.test.ts b/x-pack/plugins/apm/common/utils/environment_query.test.ts index a4ffec0d64d3e..fbd1b6b9a7a2d 100644 --- a/x-pack/plugins/apm/common/utils/environment_query.test.ts +++ b/x-pack/plugins/apm/common/utils/environment_query.test.ts @@ -10,9 +10,9 @@ import { ENVIRONMENT_NOT_DEFINED } from '../environment_filter_values'; import { environmentQuery } from './environment_query'; describe('environmentQuery', () => { - describe('when environment is undefined', () => { + describe('when environment is an empty string', () => { it('returns an empty query', () => { - expect(environmentQuery()).toEqual([]); + expect(environmentQuery('')).toEqual([]); }); }); diff --git a/x-pack/plugins/apm/common/utils/environment_query.ts b/x-pack/plugins/apm/common/utils/environment_query.ts index acc75c13a0e35..7b35f90d87691 100644 --- a/x-pack/plugins/apm/common/utils/environment_query.ts +++ b/x-pack/plugins/apm/common/utils/environment_query.ts @@ -13,7 +13,7 @@ import { } from '../environment_filter_values'; export function environmentQuery( - environment?: string + environment: string ): QueryDslQueryContainer[] { if (!environment || environment === ENVIRONMENT_ALL.value) { return []; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index e7fe80a60e57a..e26261035c084 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -48,7 +48,7 @@ describe('Home page', () => { it('includes services with only metric documents', () => { cy.visit( - `${serviceInventoryHref}&kuery=not%2520(processor.event%2520%253A%2522transaction%2522%2520)` + `${serviceInventoryHref}&kuery=not%20(processor.event%3A%22transaction%22)` ); cy.contains('opbeans-python'); cy.contains('opbeans-java'); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index abcacbe89587b..4e0867553f421 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -34,6 +34,7 @@ import { import { TRANSACTION_PAGE_LOAD } from '../../../../../common/transaction_types'; import { useIndexPattern } from './use_index_pattern'; import { environmentQuery } from './queries'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; const filterNames: UxLocalUIFilterName[] = [ 'location', @@ -71,7 +72,7 @@ function LocalUIFilters() { const getFilters = useMemo(() => { const dataFilters: ESFilter[] = [ ...RUM_DATA_FILTERS, - ...environmentQuery(environment), + ...environmentQuery(environment || ENVIRONMENT_ALL.value), ]; if (serviceName) { dataFilters.push({ diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts index 12f1fc0f0faea..b20a122b35a7f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/queries.ts @@ -15,7 +15,7 @@ import { type QueryDslQueryContainer = ESFilter; export function environmentQuery( - environment?: string + environment: string ): QueryDslQueryContainer[] { if (!environment || environment === ENVIRONMENT_ALL.value) { return []; diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_trace_page_url.ts b/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_trace_page_url.ts index 673b48ce63e98..a94ecd5c8f875 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_trace_page_url.ts +++ b/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_trace_page_url.ts @@ -20,7 +20,7 @@ export const getRedirectToTracePageUrl = ({ format({ pathname: `/traces`, query: { - kuery: encodeURIComponent(`${TRACE_ID} : "${traceId}"`), + kuery: `${TRACE_ID} : "${traceId}"`, rangeFrom, rangeTo, }, diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx index 0661b5ddc871d..58e1b8d7a6770 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/trace_link.test.tsx @@ -97,7 +97,7 @@ describe('TraceLink', () => { const component = shallow(); expect(component.prop('to')).toEqual( - '/traces?kuery=trace.id%2520%253A%2520%2522123%2522&rangeFrom=now-24h&rangeTo=now' + '/traces?kuery=trace.id%20%3A%20%22123%22&rangeFrom=now-24h&rangeTo=now' ); }); }); diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx index 425506a3e035a..5b4ecb8e73752 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx @@ -20,11 +20,11 @@ import { DependenciesTableServiceMapLink } from '../../shared/dependencies_table export function BackendDetailDependenciesTable() { const { - urlParams: { start, end, environment, comparisonEnabled, comparisonType }, + urlParams: { start, end, comparisonEnabled, comparisonType }, } = useUrlParams(); const { - query: { rangeFrom, rangeTo, kuery }, + query: { rangeFrom, rangeTo, kuery, environment }, } = useApmParams('/backends/:backendName/overview'); const router = useApmRouter(); @@ -34,6 +34,7 @@ export function BackendDetailDependenciesTable() { rangeFrom, rangeTo, environment, + kuery, }, }); diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx index e2c572c46a3da..16ab5cefdc658 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx @@ -8,7 +8,6 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { asPercent } from '../../../../common/utils/formatters'; import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useComparison } from '../../../hooks/use_comparison'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; @@ -31,15 +30,11 @@ export function BackendFailedTransactionRateChart({ const theme = useTheme(); const { - query: { rangeFrom, rangeTo }, + query: { kuery, environment, rangeFrom, rangeTo }, } = useApmParams('/backends/:backendName/overview'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { - urlParams: { kuery, environment }, - } = useUrlParams(); - const { offset, comparisonChartTheme } = useComparison(); const { data, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx index 472295e69c20a..99f46e77b60f1 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx @@ -8,7 +8,6 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { getDurationFormatter } from '../../../../common/utils/formatters'; import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useComparison } from '../../../hooks/use_comparison'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; @@ -27,15 +26,11 @@ export function BackendLatencyChart({ height }: { height: number }) { const theme = useTheme(); const { - query: { rangeFrom, rangeTo }, + query: { rangeFrom, rangeTo, kuery, environment }, } = useApmParams('/backends/:backendName/overview'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { - urlParams: { kuery, environment }, - } = useUrlParams(); - const { offset, comparisonChartTheme } = useComparison(); const { data, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx index 6088d315866db..ba4bdafe94bdf 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx @@ -8,7 +8,6 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { asTransactionRate } from '../../../../common/utils/formatters'; import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useComparison } from '../../../hooks/use_comparison'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; @@ -23,15 +22,11 @@ export function BackendThroughputChart({ height }: { height: number }) { const theme = useTheme(); const { - query: { rangeFrom, rangeTo }, + query: { rangeFrom, rangeTo, kuery, environment }, } = useApmParams('/backends/:backendName/overview'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { - urlParams: { kuery, environment }, - } = useUrlParams(); - const { offset, comparisonChartTheme } = useComparison(); const { data, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index 39d13247bc5e4..37fabcf37a7ad 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -27,7 +27,7 @@ import { BackendDetailTemplate } from '../../routing/templates/backend_detail_te export function BackendDetailOverview() { const { path: { backendName }, - query: { rangeFrom, rangeTo }, + query: { rangeFrom, rangeTo, environment, kuery }, } = useApmParams('/backends/:backendName/overview'); const apmRouter = useApmRouter(); @@ -35,7 +35,9 @@ export function BackendDetailOverview() { useBreadcrumb([ { title: BackendInventoryTitle, - href: apmRouter.link('/backends', { query: { rangeFrom, rangeTo } }), + href: apmRouter.link('/backends', { + query: { rangeFrom, rangeTo, environment, kuery }, + }), }, { title: backendName, @@ -44,6 +46,8 @@ export function BackendDetailOverview() { query: { rangeFrom, rangeTo, + environment, + kuery, }, }), }, diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx index eff744fa45aee..cfee03ef36095 100644 --- a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx @@ -21,11 +21,11 @@ import { useUiTracker } from '../../../../../../observability/public'; export function BackendInventoryDependenciesTable() { const { - urlParams: { start, end, environment, comparisonEnabled, comparisonType }, + urlParams: { start, end, comparisonEnabled, comparisonType }, } = useUrlParams(); const { - query: { rangeFrom, rangeTo, kuery }, + query: { rangeFrom, rangeTo, environment, kuery }, } = useApmParams('/backends'); const router = useApmRouter(); @@ -37,6 +37,7 @@ export function BackendInventoryDependenciesTable() { rangeFrom, rangeTo, environment, + kuery, }, }); diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index 7202231fa66b2..298206f30d614 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -22,6 +22,7 @@ import { useUiTracker } from '../../../../../observability/public'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { useApmParams } from '../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; @@ -53,14 +54,7 @@ export function ErrorCorrelations({ onClose }: Props) { const { serviceName } = useApmServiceContext(); const { urlParams } = useUrlParams(); - const { - environment, - kuery, - transactionName, - transactionType, - start, - end, - } = urlParams; + const { transactionName, transactionType, start, end } = urlParams; const { defaultFieldNames } = useFieldNames(); const [fieldNames, setFieldNames] = useLocalStorage( `apm.correlations.errors.fields:${serviceName}`, @@ -68,6 +62,10 @@ export function ErrorCorrelations({ onClose }: Props) { ); const hasFieldNames = fieldNames.length > 0; + const { + query: { environment, kuery }, + } = useApmParams('/services/:serviceName'); + const { data: overallData, status: overallStatus } = useFetcher( (callApmApi) => { if (start && end) { diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index 0bb7ac3c17c42..57ba75d945ee5 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -40,16 +40,14 @@ import { IStickyProperty, StickyProperties, } from '../../shared/sticky_properties'; -import { - getEnvironmentLabel, - getNextEnvironmentUrlParam, -} from '../../../../common/environment_filter_values'; +import { getEnvironmentLabel } from '../../../../common/environment_filter_values'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useApmParams } from '../../../hooks/use_apm_params'; const errorRateTab = { key: 'errorRate', @@ -73,6 +71,10 @@ export function Correlations() { const { urlParams } = useUrlParams(); const { serviceName } = useApmServiceContext(); + const { + query: { environment }, + } = useApmParams('/services/:serviceName'); + const history = useHistory(); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [currentTab, setCurrentTab] = useState(latencyCorrelationsTab.key); @@ -89,13 +91,8 @@ export function Correlations() { useTrackMetric({ ...metric, delay: 15000 }); const stickyProperties: IStickyProperty[] = useMemo(() => { - const nextEnvironment = getNextEnvironmentUrlParam({ - requestedEnvironment: serviceName, - currentEnvironmentUrlParam: urlParams.environment, - }); - const properties: IStickyProperty[] = []; - if (serviceName !== undefined && nextEnvironment !== undefined) { + if (serviceName !== undefined) { properties.push({ label: i18n.translate('xpack.apm.correlations.serviceLabel', { defaultMessage: 'Service', @@ -106,16 +103,14 @@ export function Correlations() { }); } - if (urlParams.environment) { - properties.push({ - label: i18n.translate('xpack.apm.correlations.environmentLabel', { - defaultMessage: 'Environment', - }), - fieldName: SERVICE_ENVIRONMENT, - val: getEnvironmentLabel(urlParams.environment), - width: '20%', - }); - } + properties.push({ + label: i18n.translate('xpack.apm.correlations.environmentLabel', { + defaultMessage: 'Environment', + }), + fieldName: SERVICE_ENVIRONMENT, + val: getEnvironmentLabel(environment), + width: '20%', + }); if (urlParams.transactionName) { properties.push({ @@ -129,7 +124,7 @@ export function Correlations() { } return properties; - }, [serviceName, urlParams.environment, urlParams.transactionName]); + }, [serviceName, environment, urlParams.transactionName]); return ( <> diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 400a9f227959f..0871447337780 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -31,6 +31,7 @@ import { useFieldNames } from './use_field_names'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useUiTracker } from '../../../../../observability/public'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useApmParams } from '../../../hooks/use_apm_params'; type OverallLatencyApiResponse = NonNullable< APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'> @@ -51,15 +52,13 @@ export function LatencyCorrelations({ onClose }: Props) { ] = useState(null); const { serviceName } = useApmServiceContext(); - const { urlParams } = useUrlParams(); + const { - environment, - kuery, - transactionName, - transactionType, - start, - end, - } = urlParams; + query: { kuery, environment }, + } = useApmParams('/services/:serviceName'); + + const { urlParams } = useUrlParams(); + const { transactionName, transactionType, start, end } = urlParams; const { defaultFieldNames } = useFieldNames(); const [fieldNames, setFieldNames] = useLocalStorage( `apm.correlations.latency.fields:${serviceName}`, diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index cf4cc68865977..06d7b1ba585d5 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -94,14 +94,14 @@ function ErrorGroupHeader({ export function ErrorGroupDetails() { const { urlParams } = useUrlParams(); - const { environment, kuery, start, end } = urlParams; + const { start, end } = urlParams; const { serviceName } = useApmServiceContext(); const apmRouter = useApmRouter(); const { path: { groupId }, - query: { rangeFrom, rangeTo }, + query: { rangeFrom, rangeTo, environment, kuery }, } = useApmParams('/services/:serviceName/errors/:groupId'); useBreadcrumb({ @@ -114,6 +114,8 @@ export function ErrorGroupDetails() { query: { rangeFrom, rangeTo, + environment, + kuery, }, }), }); @@ -144,6 +146,8 @@ export function ErrorGroupDetails() { const { errorDistributionData } = useErrorGroupDistributionFetcher({ serviceName, groupId, + environment, + kuery, }); if (!errorGroupData || !errorDistributionData) { diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 910f6aba3303d..d8f96db6ed9e2 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -36,6 +36,8 @@ export function ErrorGroupOverview() { const { errorDistributionData } = useErrorGroupDistributionFetcher({ serviceName, groupId: undefined, + environment, + kuery, }); const { data: errorGroupListData } = useFetcher( diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index b807510106f01..d0cf5c9893750 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -14,6 +14,7 @@ import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detecti import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { useApmParams } from '../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; @@ -35,16 +36,15 @@ const initialData = { let hasDisplayedToast = false; -function useServicesFetcher() { +function useServicesFetcher({ + environment, + kuery, +}: { + environment: string; + kuery: string; +}) { const { - urlParams: { - environment, - kuery, - start, - end, - comparisonEnabled, - comparisonType, - }, + urlParams: { start, end, comparisonEnabled, comparisonType }, } = useUrlParams(); const { core } = useApmPluginContext(); const upgradeAssistantHref = useUpgradeAssistantHref(); @@ -155,13 +155,20 @@ function useServicesFetcher() { export function ServiceInventory() { const { core } = useApmPluginContext(); - const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); + + const { + query: { environment, kuery }, + } = useApmParams('/services'); + + const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ + kuery, + }); const { mainStatisticsData, mainStatisticsStatus, comparisonData, isLoading, - } = useServicesFetcher(); + } = useServicesFetcher({ environment, kuery }); const { anomalyDetectionJobsData, diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx index 51636e938f8cb..694ecd691da33 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx @@ -13,6 +13,7 @@ import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; import { getServiceColumns, ServiceList } from './'; import { items } from './__fixtures__/service_api_mock_data'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; function Wrapper({ children }: { children?: ReactNode }) { return ( @@ -47,6 +48,8 @@ describe('ServiceList', () => { const query = { rangeFrom: 'now-15m', rangeTo: 'now', + environment: ENVIRONMENT_ALL.value, + kuery: '', }; const service: any = { diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx index 8e642c1f27e1a..cdc1dbea773be 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -21,11 +21,17 @@ import { HOSTNAME, POD_NAME, } from '../../../../common/elasticsearch_fieldnames'; +import { useApmParams } from '../../../hooks/use_apm_params'; export function ServiceLogs() { const { serviceName } = useApmServiceContext(); + + const { + query: { environment, kuery }, + } = useApmParams('/services/:serviceName/logs'); + const { - urlParams: { environment, kuery, start, end }, + urlParams: { start, end }, } = useUrlParams(); const { data, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx index faf807d4d4fc3..31b1b185f7478 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { Popover } from '.'; import { createKibanaReactContext } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; @@ -97,7 +98,7 @@ const stories: Meta = { export default stories; export const Backend: Story = () => { - return ; + return ; }; Backend.args = { nodeData: { @@ -110,7 +111,7 @@ Backend.args = { }; export const BackendWithLongTitle: Story = () => { - return ; + return ; }; BackendWithLongTitle.args = { nodeData: { @@ -124,14 +125,14 @@ BackendWithLongTitle.args = { }; export const ExternalsList: Story = () => { - return ; + return ; }; ExternalsList.args = { nodeData: exampleGroupedConnectionsData, }; export const Resource: Story = () => { - return ; + return ; }; Resource.args = { nodeData: { @@ -144,7 +145,7 @@ Resource.args = { }; export const Service: Story = () => { - return ; + return ; }; Service.args = { nodeData: { diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index 43308cfdce9f1..5a55fd1979c91 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -20,11 +20,11 @@ import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { ApmRoutes } from '../../../routing/apm_route_config'; import { StatsList } from './stats_list'; -export function BackendContents({ nodeData }: ContentsProps) { +export function BackendContents({ nodeData, environment }: ContentsProps) { const { query } = useApmParams('/*'); const apmRouter = useApmRouter(); const { - urlParams: { environment, start, end }, + urlParams: { start, end }, } = useUrlParams(); const backendName = nodeData.label; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/index.tsx index df923730a2227..52e0568a5602b 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/index.tsx @@ -26,6 +26,7 @@ import { SERVICE_NAME, SPAN_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; +import { Environment } from '../../../../../common/environment_rt'; import { useTheme } from '../../../../hooks/use_theme'; import { CytoscapeContext } from '../Cytoscape'; import { getAnimationOptions, popoverWidth } from '../cytoscape_options'; @@ -53,14 +54,22 @@ function getContentsComponent(selectedNodeData: cytoscape.NodeDataDefinition) { export interface ContentsProps { nodeData: cytoscape.NodeDataDefinition; + environment: Environment; + kuery: string; onFocusClick: (event: MouseEvent) => void; } interface PopoverProps { focusedServiceName?: string; + environment: Environment; + kuery: string; } -export function Popover({ focusedServiceName }: PopoverProps) { +export function Popover({ + focusedServiceName, + environment, + kuery, +}: PopoverProps) { const theme = useTheme(); const cy = useContext(CytoscapeContext); const [selectedNode, setSelectedNode] = useState< @@ -171,6 +180,8 @@ export function Popover({ focusedServiceName }: PopoverProps) { diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx index 274e25b342d2a..eb13a854925c4 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx @@ -13,20 +13,20 @@ import React from 'react'; import { useApmParams } from '../../../../hooks/use_apm_params'; import type { ContentsProps } from '.'; import { NodeStats } from '../../../../../common/service_map'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { AnomalyDetection } from './anomaly_detection'; import { StatsList } from './stats_list'; import { useTimeRange } from '../../../../hooks/use_time_range'; -export function ServiceContents({ onFocusClick, nodeData }: ContentsProps) { +export function ServiceContents({ + onFocusClick, + nodeData, + environment, + kuery, +}: ContentsProps) { const apmRouter = useApmRouter(); - const { - urlParams: { environment }, - } = useUrlParams(); - const { query } = useApmParams('/*'); if ( @@ -65,12 +65,12 @@ export function ServiceContents({ onFocusClick, nodeData }: ContentsProps) { const detailsUrl = apmRouter.link('/services/:serviceName', { path: { serviceName }, - query: { rangeFrom, rangeTo }, + query: { rangeFrom, rangeTo, environment, kuery }, }); const focusUrl = apmRouter.link('/services/:serviceName/service-map', { path: { serviceName }, - query: { rangeFrom, rangeTo }, + query: { rangeFrom, rangeTo, environment, kuery }, }); const { serviceAnomalyStats } = nodeData; diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.test.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.test.tsx index f68d8e46f66e3..0259ac367b126 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.test.tsx @@ -18,6 +18,7 @@ import * as useFetcherModule from '../../../hooks/use_fetcher'; import { ServiceMap } from '.'; import { UrlParamsProvider } from '../../../context/url_params_context/url_params_context'; import { Router } from 'react-router-dom'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; const history = createMemoryHistory(); @@ -69,9 +70,12 @@ describe('ServiceMap', () => { describe('with no license', () => { it('renders null', async () => { expect( - await render(, { - wrapper: createWrapper(null), - }).queryByTestId('ServiceMap') + await render( + , + { + wrapper: createWrapper(null), + } + ).queryByTestId('ServiceMap') ).not.toBeInTheDocument(); }); }); @@ -79,9 +83,12 @@ describe('ServiceMap', () => { describe('with an expired license', () => { it('renders the license banner', async () => { expect( - await render(, { - wrapper: createWrapper(expiredLicense), - }).findAllByText(/Platinum/) + await render( + , + { + wrapper: createWrapper(expiredLicense), + } + ).findAllByText(/Platinum/) ).toHaveLength(1); }); }); @@ -96,9 +103,12 @@ describe('ServiceMap', () => { }); expect( - await render(, { - wrapper: createWrapper(activeLicense), - }).findAllByText(/No services available/) + await render( + , + { + wrapper: createWrapper(activeLicense), + } + ).findAllByText(/No services available/) ).toHaveLength(1); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx index b10b527de25dd..22a65426f39f5 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx @@ -32,6 +32,8 @@ import { TimeoutPrompt } from './timeout_prompt'; import { useRefDimensions } from './useRefDimensions'; import { SearchBar } from '../../shared/search_bar'; import { useServiceName } from '../../../hooks/use_service_name'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { Environment } from '../../../../common/environment_rt'; function PromptContainer({ children }: { children: ReactNode }) { return ( @@ -63,9 +65,30 @@ function LoadingSpinner() { ); } -export function ServiceMap() { +export function ServiceMapHome() { + const { + query: { environment, kuery }, + } = useApmParams('/service-map'); + return ; +} + +export function ServiceMapServiceDetail() { + const { + query: { environment, kuery }, + } = useApmParams('/services/:serviceName/service-map'); + return ; +} + +export function ServiceMap({ + environment, + kuery, +}: { + environment: Environment; + kuery: string; +}) { const theme = useTheme(); const license = useLicenseContext(); + const { urlParams } = useUrlParams(); const serviceName = useServiceName(); @@ -77,7 +100,7 @@ export function ServiceMap() { return; } - const { start, end, environment } = urlParams; + const { start, end } = urlParams; if (start && end) { return callApmApi({ isCachable: false, @@ -93,7 +116,7 @@ export function ServiceMap() { }); } }, - [license, serviceName, urlParams] + [license, serviceName, environment, urlParams] ); const { ref, height } = useRefDimensions(); @@ -154,7 +177,11 @@ export function ServiceMap() { {serviceName && } {status === FETCH_STATUS.LOADING && } - +
diff --git a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx index 0bdcb966211e4..652a1ed20bc92 100644 --- a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx @@ -9,13 +9,20 @@ import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../hooks/use_apm_params'; import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher'; import { MetricsChart } from '../../shared/charts/metrics_chart'; export function ServiceMetrics() { const { urlParams } = useUrlParams(); + const { + query: { environment, kuery }, + } = useApmParams('/services/:serviceName/metrics'); + const { data, status } = useServiceMetricChartsFetcher({ serviceNodeName: undefined, + environment, + kuery, }); const { start, end } = urlParams; diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index 1b5754ef74e8b..2e3f91e1419dd 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -47,7 +47,7 @@ const Truncate = euiStyled.span` export function ServiceNodeMetrics() { const { - urlParams: { kuery, start, end }, + urlParams: { start, end }, } = useUrlParams(); const { agentName, serviceName } = useApmServiceContext(); @@ -58,6 +58,8 @@ export function ServiceNodeMetrics() { query, } = useApmParams('/services/:serviceName/nodes/:serviceNodeName/metrics'); + const { environment, kuery } = query; + useBreadcrumb({ title: getServiceNodeName(serviceNodeName), href: apmRouter.link( @@ -72,7 +74,11 @@ export function ServiceNodeMetrics() { ), }); - const { data } = useServiceMetricChartsFetcher({ serviceNodeName }); + const { data } = useServiceMetricChartsFetcher({ + serviceNodeName, + kuery, + environment, + }); const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 4346b391b3666..f36f6d4cc9b5a 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -19,6 +19,7 @@ import { } from '../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../hooks/use_apm_params'; import { useFetcher } from '../../../hooks/use_fetcher'; import { truncate, unit } from '../../../utils/style'; import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; @@ -34,7 +35,11 @@ const ServiceNodeName = euiStyled.div` function ServiceNodeOverview() { const { - urlParams: { kuery, start, end }, + query: { environment, kuery }, + } = useApmParams('/services/:serviceName/nodes'); + + const { + urlParams: { start, end }, } = useUrlParams(); const { serviceName } = useApmServiceContext(); @@ -52,13 +57,14 @@ function ServiceNodeOverview() { }, query: { kuery, + environment, start, end, }, }, }); }, - [kuery, serviceName, start, end] + [kuery, environment, serviceName, start, end] ); const items = data?.serviceNodes ?? []; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index a261095fbe2f6..a9fdd11805840 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -20,6 +20,7 @@ import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; import { ServiceOverviewInstancesChartAndTable } from './service_overview_instances_chart_and_table'; import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart'; import { TransactionsTable } from '../../shared/transactions_table'; +import { useApmParams } from '../../../hooks/use_apm_params'; import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; @@ -30,8 +31,13 @@ import { AggregatedTransactionsCallout } from '../../shared/aggregated_transacti export const chartHeight = 288; export function ServiceOverview() { - const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { agentName, serviceName } = useApmServiceContext(); + const { + query: { environment, kuery }, + } = useApmParams('/services/:serviceName/overview'); + const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ + kuery, + }); // The default EuiFlexGroup breaks at 768, but we want to break at 992, so we // observe the window width and set the flex directions of rows accordingly @@ -41,7 +47,10 @@ export function ServiceOverview() { const isIosAgent = isIosAgentName(agentName); return ( - + {fallbackToTransactions && ( @@ -51,7 +60,11 @@ export function ServiceOverview() { )} - + @@ -61,11 +74,15 @@ export function ServiceOverview() { responsive={false} > - + - + @@ -81,6 +98,8 @@ export function ServiceOverview() { )} @@ -98,7 +117,11 @@ export function ServiceOverview() { responsive={false} > - + {!isRumAgent && ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index 085c69bb9eecf..642725727eedf 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -26,7 +26,6 @@ export function ServiceOverviewDependenciesTable() { urlParams: { start, end, - environment, comparisonEnabled, comparisonType, latencyAggregationType, @@ -35,7 +34,7 @@ export function ServiceOverviewDependenciesTable() { const { query, - query: { kuery, rangeFrom, rangeTo }, + query: { environment, kuery, rangeFrom, rangeTo }, } = useApmParams('/services/:serviceName/*'); const { offset } = getTimeRangeComparison({ diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index 2aad045406f9f..68e6873caf2f8 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -24,6 +24,7 @@ import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; import { OverviewTableContainer } from '../../../shared/overview_table_container'; import { getColumns } from './get_column'; +import { useApmParams } from '../../../../hooks/use_apm_params'; interface Props { serviceName: string; @@ -57,14 +58,7 @@ const INITIAL_STATE_DETAILED_STATISTICS: ErrorGroupDetailedStatistics = { export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { - urlParams: { - environment, - kuery, - start, - end, - comparisonType, - comparisonEnabled, - }, + urlParams: { start, end, comparisonType, comparisonEnabled }, } = useUrlParams(); const { transactionType } = useApmServiceContext(); const [tableOptions, setTableOptions] = useState<{ @@ -88,6 +82,10 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { pageIndex, sort } = tableOptions; const { direction, field } = sort; + const { + query: { environment, kuery }, + } = useApmParams('/services/:serviceName/overview'); + const { data = INITIAL_STATE_MAIN_STATISTICS, status } = useFetcher( (callApmApi) => { if (!start || !end || !transactionType) { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index 7723e55043e42..022f9f89a69f5 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -11,6 +11,7 @@ import React, { useState } from 'react'; import uuid from 'uuid'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { InstancesLatencyDistributionChart } from '../../shared/charts/instances_latency_distribution_chart'; @@ -68,10 +69,12 @@ export function ServiceOverviewInstancesChartAndTable({ const { pageIndex, sort } = tableOptions; const { direction, field } = sort; + const { + query: { environment, kuery }, + } = useApmParams('/services/:serviceName/overview'); + const { urlParams: { - environment, - kuery, latencyAggregationType, start, end, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index a557011a0a3e0..718d6878c7c99 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -33,20 +33,17 @@ const INITIAL_STATE = { export function ServiceOverviewThroughputChart({ height, + environment, + kuery, }: { height?: number; + environment: string; + kuery: string; }) { const theme = useTheme(); const { - urlParams: { - environment, - kuery, - start, - end, - comparisonEnabled, - comparisonType, - }, + urlParams: { start, end, comparisonEnabled, comparisonType }, } = useUrlParams(); const { transactionType, serviceName } = useApmServiceContext(); diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index 82195bc5b4d17..84577036896c3 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -25,11 +25,11 @@ export function ServiceProfiling() { const { serviceName } = useApmServiceContext(); const { - query: { environment }, + query: { environment, kuery }, } = useApmParams('/services/:serviceName/profiling'); const { - urlParams: { kuery, start, end }, + urlParams: { start, end }, } = useUrlParams(); const { data = DEFAULT_DATA } = useFetcher( diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx index 82ac3a04f63f1..a2ce4dcf7e83f 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx @@ -129,8 +129,8 @@ export function ServiceProfilingFlamegraph({ end, }: { serviceName: string; - environment?: string; - kuery?: string; + environment: string; + kuery: string; valueType?: ProfilingValueType; start?: string; end?: string; diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index 76e537dc66c0c..7272f9d5ab211 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { SearchBar } from '../../shared/search_bar'; @@ -21,9 +22,15 @@ const DEFAULT_RESPONSE: TracesAPIResponse = { }; export function TraceOverview() { - const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { - urlParams: { environment, kuery, start, end }, + query: { environment, kuery }, + } = useApmParams('/traces'); + const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ + kuery, + }); + + const { + urlParams: { start, end }, } = useUrlParams(); const { status, data = DEFAULT_RESPONSE } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 6f0639de93e43..143e82649facd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -49,7 +49,11 @@ export function TransactionDetails() { const { distributionData, distributionStatus, - } = useTransactionDistributionFetcher({ transactionName }); + } = useTransactionDistributionFetcher({ + transactionName, + environment: query.environment, + kuery: query.kuery, + }); useBreadcrumb({ title: transactionName, @@ -100,7 +104,10 @@ export function TransactionDetails() { - + diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx index a0e9c835b098d..c5828dea2c920 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/MaybeViewTraceLink.tsx @@ -13,16 +13,19 @@ import { useUrlParams } from '../../../../context/url_params_context/use_url_par import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/transaction'; import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; +import { Environment } from '../../../../../common/environment_rt'; export const MaybeViewTraceLink = ({ transaction, waterfall, + environment, }: { transaction: ITransaction; waterfall: IWaterfall; + environment: Environment; }) => { const { - urlParams: { environment, latencyAggregationType }, + urlParams: { latencyAggregationType }, } = useUrlParams(); const viewFullTraceButtonLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index ffcbb8cbfd382..64c4e7dcb42b9 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -26,6 +26,7 @@ import { TransactionActionMenu } from '../../../shared/transaction_action_menu/T import { MaybeViewTraceLink } from './MaybeViewTraceLink'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; +import { useApmParams } from '../../../../hooks/use_apm_params'; type DistributionApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; @@ -49,6 +50,10 @@ export function WaterfallWithSummary({ const history = useHistory(); const [sampleActivePage, setSampleActivePage] = useState(0); + const { + query: { environment }, + } = useApmParams('/services/:serviceName/transactions/view'); + useEffect(() => { setSampleActivePage(0); }, [traceSamples]); @@ -116,6 +121,7 @@ export function WaterfallWithSummary({ diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx index bce59b02f102c..97e353d22ccf6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx @@ -56,7 +56,10 @@ export function StickySpanProperties({ span, transaction }: Props) { val: ( ), diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx index cd892ae2a16eb..3af010fb30b86 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx @@ -220,9 +220,7 @@ export function WaterfallItem({ )} - + diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 1502038b378b2..ce74a48fd8c86 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -9,11 +9,13 @@ import { Outlet } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; import { Redirect } from 'react-router-dom'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { environmentRt } from '../../../../common/environment_rt'; import { BackendDetailOverview } from '../../app/backend_detail_overview'; import { BackendInventory } from '../../app/backend_inventory'; import { Breadcrumb } from '../../app/breadcrumb'; import { ServiceInventory } from '../../app/service_inventory'; -import { ServiceMap } from '../../app/service_map'; +import { ServiceMapHome } from '../../app/service_map'; import { TraceOverview } from '../../app/trace_overview'; import { ApmMainTemplate } from '../templates/apm_main_template'; @@ -55,13 +57,11 @@ export const home = { element: , params: t.type({ query: t.intersection([ - t.partial({ - environment: t.string, - kuery: t.string, - }), + environmentRt, t.type({ rangeFrom: t.string, rangeTo: t.string, + kuery: t.string, }), ]), }), @@ -69,6 +69,8 @@ export const home = { query: { rangeFrom: 'now-15m', rangeTo: 'now', + environment: ENVIRONMENT_ALL.value, + kuery: '', }, }, children: [ @@ -89,7 +91,7 @@ export const home = { title: i18n.translate('xpack.apm.views.serviceMap.title', { defaultMessage: 'Service Map', }), - element: , + element: , }), { path: '/backends', diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 5aeae224d0d5d..ce9487108be64 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -8,6 +8,8 @@ import * as t from 'io-ts'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Outlet } from '@kbn/typed-react-router-config'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { environmentRt } from '../../../../common/environment_rt'; import { ServiceOverview } from '../../app/service_overview'; import { ApmServiceTemplate } from '../templates/apm_service_template'; import { RedirectToDefaultServiceRouteView } from './redirect_to_default_service_route_view'; @@ -18,7 +20,7 @@ import { ErrorGroupDetails } from '../../app/error_group_details'; import { ServiceMetrics } from '../../app/service_metrics'; import { ServiceNodeOverview } from '../../app/service_node_overview'; import { ServiceNodeMetrics } from '../../app/service_node_metrics'; -import { ServiceMap } from '../../app/service_map'; +import { ServiceMapServiceDetail } from '../../app/service_map'; import { TransactionDetails } from '../../app/transaction_details'; import { ServiceProfiling } from '../../app/service_profiling'; import { ServiceDependencies } from '../../app/service_dependencies'; @@ -69,17 +71,17 @@ export const serviceDetail = { }), t.type({ query: t.intersection([ + environmentRt, t.type({ rangeFrom: t.string, rangeTo: t.string, + kuery: t.string, }), t.partial({ - environment: t.string, comparisonEnabled: t.string, comparisonType: t.string, latencyAggregationType: t.string, transactionType: t.string, - kuery: t.string, }), ]), }), @@ -88,6 +90,8 @@ export const serviceDetail = { query: { rangeFrom: 'now-15m', rangeTo: 'now', + kuery: '', + environment: ENVIRONMENT_ALL.value, }, }, children: [ @@ -229,7 +233,7 @@ export const serviceDetail = { title: i18n.translate('xpack.apm.views.serviceMap.title', { defaultMessage: 'Service Map', }), - element: , + element: , searchBarOptions: { hidden: true, }, diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index cc1e79c9687f5..084b8d1ce6840 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -22,10 +22,9 @@ import { useApmParams } from '../../../hooks/use_apm_params'; function updateEnvironmentUrl( history: History, location: ReturnType, - environment?: string + environment: string ) { - const nextEnvironmentQueryParam = - environment !== ENVIRONMENT_ALL.value ? environment : undefined; + const nextEnvironmentQueryParam = environment; history.push({ ...location, search: fromQuery({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx index ff9e46fc0552a..20c623d6792e6 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx @@ -33,6 +33,8 @@ const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_N interface Props { height?: number; + kuery: string; + environment: string; } const options: Array<{ value: LatencyAggregationType; text: string }> = [ @@ -45,7 +47,7 @@ function filterNil(value: T | null | undefined): value is T { return value != null; } -export function LatencyChart({ height }: Props) { +export function LatencyChart({ height, kuery, environment }: Props) { const history = useHistory(); const theme = useTheme(); const comparisonChartTheme = getComparisonChartTheme(theme); @@ -56,7 +58,10 @@ export function LatencyChart({ height }: Props) { const { latencyChartsData, latencyChartsStatus, - } = useTransactionLatencyChartsFetcher(); + } = useTransactionLatencyChartsFetcher({ + kuery, + environment, + }); const { currentPeriod, diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx index 39b7f488d68e6..f9b22c422e3e3 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx @@ -24,6 +24,7 @@ import { StoryContext } from '@storybook/react'; import React, { ComponentType } from 'react'; import { MemoryRouter, Route } from 'react-router-dom'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; import { ApmPluginContext, @@ -119,7 +120,9 @@ export default { }; export function Example(_args: Args) { - return ; + return ( + + ); } Example.args = { alertsResponse: { @@ -801,7 +804,9 @@ Example.args = { }; export function NoData(_args: Args) { - return ; + return ( + + ); } NoData.args = { alertsResponse: { alerts: [] }, diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx index 0e2b1e185f9d9..6f18b649502d5 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx @@ -15,11 +15,15 @@ import { BreakdownChart } from '../breakdown_chart'; export function TransactionBreakdownChart({ height, showAnnotations = true, + environment, + kuery, }: { height?: number; showAnnotations?: boolean; + environment: string; + kuery: string; }) { - const { data, status } = useTransactionBreakdown(); + const { data, status } = useTransactionBreakdown({ environment, kuery }); const { annotations } = useAnnotationsContext(); const { timeseries } = data; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts index 233d0821a5319..29c47489bd104 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts @@ -9,9 +9,15 @@ import { useFetcher } from '../../../../hooks/use_fetcher'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; -export function useTransactionBreakdown() { +export function useTransactionBreakdown({ + kuery, + environment, +}: { + kuery: string; + environment: string; +}) { const { - urlParams: { environment, kuery, start, end, transactionName }, + urlParams: { start, end, transactionName }, } = useUrlParams(); const { transactionType, serviceName } = useApmServiceContext(); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index 3135be151fce7..dab6b9a7c5562 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -14,20 +14,29 @@ import { LatencyChart } from '../latency_chart'; import { TransactionBreakdownChart } from '../transaction_breakdown_chart'; import { TransactionErrorRateChart } from '../transaction_error_rate_chart/'; -export function TransactionCharts() { +export function TransactionCharts({ + kuery, + environment, +}: { + kuery: string; + environment: string; +}) { return ( <> - + - + - + @@ -35,10 +44,16 @@ export function TransactionCharts() { - + - + diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 1685051bb44e8..930b54717f22c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -29,6 +29,8 @@ function yLabelFormat(y?: number | null) { interface Props { height?: number; showAnnotations?: boolean; + kuery: string; + environment: string; } type ErrorRate = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/error_rate'>; @@ -49,12 +51,12 @@ const INITIAL_STATE: ErrorRate = { export function TransactionErrorRateChart({ height, showAnnotations = true, + environment, + kuery, }: Props) { const theme = useTheme(); const { urlParams: { - environment, - kuery, start, end, transactionName, diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/kuery_bar/get_bool_filter.ts index 50db1db8f38fe..a68e086a16c85 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/get_bool_filter.ts @@ -13,6 +13,7 @@ import { TRANSACTION_NAME, TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { UIProcessorEvent } from '../../../../common/processor_event'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { IUrlParams } from '../../../context/url_params_context/types'; @@ -21,11 +22,13 @@ export function getBoolFilter({ groupId, processorEvent, serviceName, + environment, urlParams, }: { groupId?: string; processorEvent?: UIProcessorEvent; serviceName?: string; + environment?: string; urlParams: IUrlParams; }) { const boolFilter: ESFilter[] = []; @@ -36,7 +39,7 @@ export function getBoolFilter({ }); } - boolFilter.push(...environmentQuery(urlParams.environment)); + boolFilter.push(...environmentQuery(environment ?? ENVIRONMENT_ALL.value)); if (urlParams.transactionType) { boolFilter.push({ diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx index 8358837cac563..bd40d63d636e0 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx @@ -35,10 +35,12 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IndexPattern) { } export function KueryBar(props: { prepend?: React.ReactNode | string }) { - const { path } = useApmParams('/*'); + const { path, query } = useApmParams('/*'); const serviceName = 'serviceName' in path ? path.serviceName : undefined; const groupId = 'groupId' in path ? path.groupId : undefined; + const environment = 'environment' in query ? query.environment : undefined; + const kuery = 'kuery' in query ? query.kuery : undefined; const history = useHistory(); const [state, setState] = useState({ @@ -97,6 +99,7 @@ export function KueryBar(props: { prepend?: React.ReactNode | string }) { groupId, processorEvent, serviceName, + environment, urlParams, }), query: inputValue, @@ -137,7 +140,7 @@ export function KueryBar(props: { prepend?: React.ReactNode | string }) { ...location, search: fromQuery({ ...toQuery(location.search), - kuery: encodeURIComponent(inputValue.trim()), + kuery: inputValue.trim(), }), }); } catch (e) { @@ -148,7 +151,7 @@ export function KueryBar(props: { prepend?: React.ReactNode | string }) { return ( >( path: TPath, optional: true -): OutputOf | undefined; +): TypeOf | undefined; export function useApmParams>( path: TPath -): OutputOf; +): TypeOf; export function useApmParams( path: string, optional?: true -): OutputOf> | undefined { +): TypeOf> | undefined { return useParams(path, optional); } diff --git a/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx index 3ef685abe0847..b0c26ce1febbb 100644 --- a/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx @@ -11,12 +11,16 @@ import { useFetcher } from './use_fetcher'; export function useErrorGroupDistributionFetcher({ serviceName, groupId, + kuery, + environment, }: { serviceName: string; groupId: string | undefined; + kuery: string; + environment: string; }) { const { - urlParams: { environment, kuery, start, end }, + urlParams: { start, end }, } = useUrlParams(); const { data } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx index 51e5429633eae..9d7c350cf67c1 100644 --- a/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx @@ -8,9 +8,9 @@ import { useUrlParams } from '../context/url_params_context/use_url_params'; import { useFetcher } from './use_fetcher'; -export function useFallbackToTransactionsFetcher() { +export function useFallbackToTransactionsFetcher({ kuery }: { kuery: string }) { const { - urlParams: { kuery, start, end }, + urlParams: { start, end }, } = useUrlParams(); const { data = { fallbackToTransactions: false } } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts index 37eca08225e8f..f041d63d2bbbb 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts @@ -17,11 +17,15 @@ const INITIAL_DATA: MetricsChartsByAgentAPIResponse = { export function useServiceMetricChartsFetcher({ serviceNodeName, + kuery, + environment, }: { serviceNodeName: string | undefined; + kuery: string; + environment: string; }) { const { - urlParams: { environment, kuery, start, end }, + urlParams: { start, end }, } = useUrlParams(); const { agentName, serviceName } = useApmServiceContext(); diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts index 8e48f386772b3..7bf01e976e923 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts @@ -24,13 +24,17 @@ const INITIAL_DATA = { export function useTransactionDistributionFetcher({ transactionName, + kuery, + environment, }: { transactionName: string; + kuery: string; + environment: string; }) { const { serviceName, transactionType } = useApmServiceContext(); const { - urlParams: { environment, kuery, start, end, transactionId, traceId }, + urlParams: { start, end, transactionId, traceId }, } = useUrlParams(); const history = useHistory(); diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts index 5ae4a138608ec..d176861db2f09 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts @@ -13,13 +13,17 @@ import { getLatencyChartSelector } from '../selectors/latency_chart_selectors'; import { useTheme } from './use_theme'; import { getTimeRangeComparison } from '../components/shared/time_comparison/get_time_range_comparison'; -export function useTransactionLatencyChartsFetcher() { +export function useTransactionLatencyChartsFetcher({ + kuery, + environment, +}: { + kuery: string; + environment: string; +}) { const { transactionType, serviceName } = useApmServiceContext(); const theme = useTheme(); const { urlParams: { - environment, - kuery, start, end, transactionName, diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index e0151c473d6e2..68cba9b397f2e 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -38,6 +38,7 @@ async function prepareBaseTsConfig() { compilerOptions: { ...config.compilerOptions, incremental: false, + composite: false, }, include: [], }, diff --git a/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts index 736b146d3d2ab..aa20b4b586335 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/lib/backends/get_error_rate_charts_for_backend.ts @@ -30,8 +30,8 @@ export async function getErrorRateChartsForBackend({ setup: Setup; start: number; end: number; - environment?: string; - kuery?: string; + environment: string; + kuery: string; offset?: string; }) { const { apmEventClient } = setup; diff --git a/x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts index 79f3ee4dbef95..9ef238fa13147 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/lib/backends/get_latency_charts_for_backend.ts @@ -30,8 +30,8 @@ export async function getLatencyChartsForBackend({ setup: Setup; start: number; end: number; - environment?: string; - kuery?: string; + environment: string; + kuery: string; offset?: string; }) { const { apmEventClient } = setup; diff --git a/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts index ae8a6db903d8a..19a26c3fcf035 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts @@ -29,8 +29,8 @@ export async function getThroughputChartsForBackend({ setup: Setup; start: number; end: number; - environment?: string; - kuery?: string; + environment: string; + kuery: string; offset?: string; }) { const { apmEventClient } = setup; diff --git a/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts b/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts index 9b361774b8a9f..15fb58345e5c0 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts +++ b/x-pack/plugins/apm/server/lib/backends/get_top_backends.ts @@ -25,9 +25,9 @@ export async function getTopBackends({ start: number; end: number; numBuckets: number; - environment?: string; + environment: string; offset?: string; - kuery?: string; + kuery: string; }) { const statsItems = await getConnectionStats({ setup, diff --git a/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts index a3f6fbc31f942..adc461f882216 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts +++ b/x-pack/plugins/apm/server/lib/backends/get_upstream_services_for_backend.ts @@ -27,8 +27,8 @@ export async function getUpstreamServicesForBackend({ end: number; backendName: string; numBuckets: number; - kuery?: string; - environment?: string; + kuery: string; + environment: string; offset?: string; }) { const statsItems = await getConnectionStats({ diff --git a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts index d6e8f3f57c91a..db932593a9181 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts @@ -19,8 +19,8 @@ import { ProcessorEvent } from '../../../common/processor_event'; export interface CorrelationsOptions { setup: Setup & SetupTimeRange; - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string | undefined; transactionType: string | undefined; transactionName: string | undefined; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts index 712343d445d44..a9d2110117d89 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts @@ -28,6 +28,7 @@ describe('get buckets', () => { environment: 'prod', serviceName: 'myServiceName', bucketSize: 10, + kuery: '', setup: { start: 1528113600000, end: 1528977600000, diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 48bdfd84b0443..0a09ad45e44f7 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -23,8 +23,8 @@ export async function getBuckets({ bucketSize, setup, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; groupId?: string; bucketSize: number; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts index 6bb43a395f235..297df8b7900af 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts @@ -20,8 +20,8 @@ export async function getErrorDistribution({ groupId, setup, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; groupId?: string; setup: Setup & SetupTimeRange; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts index a0298d1fe1fba..482076ebc1efa 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts @@ -10,6 +10,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../../utils/test_helpers'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; describe('error distribution queries', () => { let mock: SearchParamsMock; @@ -23,6 +24,8 @@ describe('error distribution queries', () => { getErrorDistribution({ serviceName: 'serviceName', setup, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); @@ -35,6 +38,8 @@ describe('error distribution queries', () => { serviceName: 'serviceName', groupId: 'foo', setup, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts index 1a05f55dc8def..14aa7572c833f 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts @@ -24,8 +24,8 @@ export async function getErrorGroupSample({ groupId, setup, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; groupId: string; setup: Setup & SetupTimeRange; diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index d705a2eb5a00c..05eee3f0e130f 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -26,8 +26,8 @@ export async function getErrorGroups({ sortDirection = 'desc', setup, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; sortField?: string; sortDirection?: 'asc' | 'desc'; diff --git a/x-pack/plugins/apm/server/lib/errors/queries.test.ts b/x-pack/plugins/apm/server/lib/errors/queries.test.ts index f5b003dd261cf..3f5cdb2f6d243 100644 --- a/x-pack/plugins/apm/server/lib/errors/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/queries.test.ts @@ -11,6 +11,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../utils/test_helpers'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; describe('error queries', () => { let mock: SearchParamsMock; @@ -25,6 +26,8 @@ describe('error queries', () => { groupId: 'groupId', serviceName: 'serviceName', setup, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); @@ -38,6 +41,8 @@ describe('error queries', () => { sortField: 'foo', serviceName: 'serviceName', setup, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); @@ -51,6 +56,8 @@ describe('error queries', () => { sortField: 'latestOccurrenceAt', serviceName: 'serviceName', setup, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts index d5c7eb596a986..e3c8f52146e94 100644 --- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts @@ -14,7 +14,7 @@ export async function getFallbackToTransactions({ kuery, }: { setup: Setup & Partial; - kuery?: string; + kuery: string; }): Promise { const searchAggregatedTransactions = config['xpack.apm.searchAggregatedTransactions']; diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts index d1174fcfcac6c..ba069a8b3feb5 100644 --- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts @@ -24,7 +24,7 @@ export async function getHasAggregatedTransactions({ start?: number; end?: number; apmEventClient: APMEventClient; - kuery?: string; + kuery: string; }) { const response = await apmEventClient.search( 'get_has_aggregated_transactions', @@ -65,7 +65,7 @@ export async function getSearchAggregatedTransactions({ start?: number; end?: number; apmEventClient: APMEventClient; - kuery?: string; + kuery: string; }): Promise { const searchAggregatedTransactions = config['xpack.apm.searchAggregatedTransactions']; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts index e4d6d2e77f73c..a2c2886542743 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts @@ -15,8 +15,8 @@ export async function getDefaultMetricsCharts({ serviceName, setup, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; setup: Setup & SetupTimeRange; }) { diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 9f8e1057ba5a9..7a22a77efddb1 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -33,8 +33,8 @@ export async function fetchAndTransformGcMetrics({ fieldName, operationName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts index 3ec40d5171694..82f30639884b5 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts @@ -38,8 +38,8 @@ function getGcRateChart({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts index 8e4416d94fb90..40c383216cb4e 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts @@ -38,8 +38,8 @@ function getGcTimeChart({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts index 191b8a0f638a9..670beec7ee690 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts @@ -59,8 +59,8 @@ export function getHeapMemoryChart({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts index 5a266b57bd598..5a10c18a00cef 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts @@ -22,8 +22,8 @@ export function getJavaMetricsCharts({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts index be222132b198f..4f93eeb826dd3 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts @@ -56,8 +56,8 @@ export async function getNonHeapMemoryChart({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts index c07ed43335569..da9d732b7c1b2 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts @@ -48,8 +48,8 @@ export async function getThreadCountChart({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts index a568d58bdd438..4f5c5b41dd660 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts @@ -59,8 +59,8 @@ export function getCPUChartData({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index 1f7860d567b03..7bab19613589f 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -77,8 +77,8 @@ export async function getMemoryChartData({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 723636bf4c299..c53100b39d5c7 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -61,8 +61,8 @@ export async function fetchAndTransformMetrics({ additionalFilters = [], operationName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts b/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts index 82fda1bb76b90..176d2611c3bd5 100644 --- a/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts +++ b/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts @@ -23,8 +23,8 @@ export async function getMetricsChartDataByAgent({ serviceNodeName, agentName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/lib/metrics/queries.test.ts b/x-pack/plugins/apm/server/lib/metrics/queries.test.ts index 601f3aef3d5dc..db745d83e5187 100644 --- a/x-pack/plugins/apm/server/lib/metrics/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/metrics/queries.test.ts @@ -15,6 +15,7 @@ import { inspectSearchParams, } from '../../utils/test_helpers'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; describe('metrics queries', () => { let mock: SearchParamsMock; @@ -22,7 +23,13 @@ describe('metrics queries', () => { const createTests = (serviceNodeName?: string) => { it('fetches cpu chart data', async () => { mock = await inspectSearchParams((setup) => - getCPUChartData({ setup, serviceName: 'foo', serviceNodeName }) + getCPUChartData({ + setup, + serviceName: 'foo', + serviceNodeName, + environment: ENVIRONMENT_ALL.value, + kuery: '', + }) ); expect(mock.params).toMatchSnapshot(); @@ -30,7 +37,13 @@ describe('metrics queries', () => { it('fetches memory chart data', async () => { mock = await inspectSearchParams((setup) => - getMemoryChartData({ setup, serviceName: 'foo', serviceNodeName }) + getMemoryChartData({ + setup, + serviceName: 'foo', + serviceNodeName, + environment: ENVIRONMENT_ALL.value, + kuery: '', + }) ); expect(mock.params).toMatchSnapshot(); @@ -38,7 +51,13 @@ describe('metrics queries', () => { it('fetches heap memory chart data', async () => { mock = await inspectSearchParams((setup) => - getHeapMemoryChart({ setup, serviceName: 'foo', serviceNodeName }) + getHeapMemoryChart({ + setup, + serviceName: 'foo', + serviceNodeName, + environment: ENVIRONMENT_ALL.value, + kuery: '', + }) ); expect(mock.params).toMatchSnapshot(); @@ -46,7 +65,13 @@ describe('metrics queries', () => { it('fetches non heap memory chart data', async () => { mock = await inspectSearchParams((setup) => - getNonHeapMemoryChart({ setup, serviceName: 'foo', serviceNodeName }) + getNonHeapMemoryChart({ + setup, + serviceName: 'foo', + serviceNodeName, + environment: ENVIRONMENT_ALL.value, + kuery: '', + }) ); expect(mock.params).toMatchSnapshot(); @@ -54,7 +79,13 @@ describe('metrics queries', () => { it('fetches thread count chart data', async () => { mock = await inspectSearchParams((setup) => - getThreadCountChart({ setup, serviceName: 'foo', serviceNodeName }) + getThreadCountChart({ + setup, + serviceName: 'foo', + serviceNodeName, + environment: ENVIRONMENT_ALL.value, + kuery: '', + }) ); expect(mock.params).toMatchSnapshot(); diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts index 76ef9fb95089f..99a358f33cf5e 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts @@ -12,6 +12,7 @@ import { import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { UxUIFilters } from '../../../../typings/ui_filters'; import { environmentQuery } from '../../../../common/utils/environment_query'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; export function getEsFilter(uiFilters: UxUIFilters, exclude?: boolean) { const localFilterValues = uiFilters; @@ -36,6 +37,8 @@ export function getEsFilter(uiFilters: UxUIFilters, exclude?: boolean) { return [ ...mappedFilters, - ...(exclude ? [] : environmentQuery(uiFilters.environment)), + ...(exclude + ? [] + : environmentQuery(uiFilters.environment || ENVIRONMENT_ALL.value)), ]; } diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts index c27757b4fae69..4b10ceb035e15 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { getQueryWithParams } from './get_query_with_params'; describe('correlations', () => { @@ -16,6 +17,8 @@ describe('correlations', () => { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }, }); expect(query).toEqual({ @@ -45,6 +48,7 @@ describe('correlations', () => { start: '2020', end: '2021', environment: 'dev', + kuery: '', percentileThresholdValue: 75, includeFrozen: false, }, @@ -100,6 +104,8 @@ describe('correlations', () => { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }, fieldName: 'actualFieldName', fieldValue: 'actualFieldValue', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts index e3f5c4a42d803..b95db6d2691f1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { getRequestBase } from './get_request_base'; describe('correlations', () => { @@ -13,6 +14,8 @@ describe('correlations', () => { const requestBase = getRequestBase({ index: 'apm-*', includeFrozen: true, + environment: ENVIRONMENT_ALL.value, + kuery: '', }); expect(requestBase).toEqual({ index: 'apm-*', @@ -24,6 +27,8 @@ describe('correlations', () => { it('defaults ignore_throttled to true', () => { const requestBase = getRequestBase({ index: 'apm-*', + environment: ENVIRONMENT_ALL.value, + kuery: '', }); expect(requestBase).toEqual({ index: 'apm-*', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts index b1ab4aad36249..5245af6cdadcd 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { fetchTransactionDurationCorrelation, @@ -20,6 +21,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; const expectations = [1, 3, 5]; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts index fb707231245d0..688af72e8f6d3 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { hasPrefixToInclude } from '../utils/has_prefix_to_include'; @@ -22,6 +23,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; describe('query_field_candidates', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts index 0ee57fd60cd68..a20720944f19b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { asyncSearchServiceLogProvider } from '../async_search_service_log'; import { asyncSearchServiceStateProvider } from '../async_search_service_state'; @@ -22,6 +23,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; describe('query_field_value_pairs', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts index a44cc6131b4cc..73df48a0d8170 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { fetchTransactionDurationFractions, @@ -19,6 +20,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts index daa40725b7307..9b2a4807d4863 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { fetchTransactionDurationHistogram, @@ -19,6 +20,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; const interval = 100; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts index b40f685645a2e..bb76769fe94b5 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { fetchTransactionDurationHistogramInterval, @@ -19,6 +20,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; describe('query_histogram_interval', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts index b909778e3e4ef..52cfe6168232d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { fetchTransactionDurationHistogramRangeSteps, @@ -19,6 +20,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; describe('query_histogram_range_steps', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts index 7f89aa52367a0..22876684bec7e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { asyncSearchServiceLogProvider } from '../async_search_service_log'; import { asyncSearchServiceStateProvider } from '../async_search_service_state'; @@ -19,6 +20,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; const expectations = [1, 3, 5]; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts index 2ca77f51be07c..cab2e283935d6 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { fetchTransactionDurationPercentiles, @@ -19,6 +20,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; describe('query_percentiles', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts index fc995ae07b3d7..839d6a33cfe05 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { fetchTransactionDurationRanges, @@ -19,6 +20,8 @@ const params = { start: '2020', end: '2021', includeFrozen: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }; const rangeSteps = [1, 3, 5]; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts index 401cda97afefb..b5ab4a072be6c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import { SearchStrategyDependencies } from 'src/plugins/data/server'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; @@ -136,6 +137,8 @@ describe('APM Correlations search strategy', () => { params = { start: '2020', end: '2021', + environment: ENVIRONMENT_ALL.value, + kuery: '', }; }); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 7ce4af41f4fec..98cfc7715d6fe 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -37,7 +37,7 @@ export async function getServiceAnomalies({ environment, }: { setup: Setup & SetupTimeRange; - environment?: string; + environment: string; }) { return withApmSpan('get_service_anomalies', async () => { const { ml, start, end } = setup; @@ -153,7 +153,7 @@ export async function getServiceAnomalies({ export async function getMLJobs( anomalyDetectors: ReturnType, - environment?: string + environment: string ) { const response = await getMlJobsWithAPMGroup(anomalyDetectors); @@ -162,7 +162,7 @@ export async function getMLJobs( const mlJobs = response.jobs.filter( (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 ); - if (environment && environment !== ENVIRONMENT_ALL.value) { + if (environment !== ENVIRONMENT_ALL.value) { const matchingMLJob = mlJobs.find( (job) => job.custom_settings?.job_tags?.environment === environment ); @@ -176,7 +176,7 @@ export async function getMLJobs( export async function getMLJobIds( anomalyDetectors: ReturnType, - environment?: string + environment: string ) { const mlJobs = await getMLJobs(anomalyDetectors, environment); return mlJobs.map((job) => job.job_id); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index d7c210da8b999..4e503e225fd47 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -25,11 +25,12 @@ import { import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; import { transformServiceMapResponses } from './transform_service_map_responses'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; export interface IEnvOptions { setup: Setup & SetupTimeRange; serviceName?: string; - environment?: string; + environment: string; searchAggregatedTransactions: boolean; logger: Logger; } @@ -90,6 +91,7 @@ async function getServicesData(options: IEnvOptions) { const projection = getServicesProjection({ setup, searchAggregatedTransactions, + kuery: '', }); let filter = [ @@ -145,7 +147,10 @@ async function getServicesData(options: IEnvOptions) { [SERVICE_NAME]: bucket.key as string, [AGENT_NAME]: (bucket.agent_name.buckets[0]?.key as string | undefined) || '', - [SERVICE_ENVIRONMENT]: options.environment || null, + [SERVICE_ENVIRONMENT]: + options.environment === ENVIRONMENT_ALL.value + ? null + : options.environment, }; }) || [] ); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_backend_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_backend_node_info.ts index 17c3191d80ff4..9455e4f2479ff 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_backend_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_backend_node_info.ts @@ -21,7 +21,7 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; interface Options { setup: Setup & SetupTimeRange; - environment?: string; + environment: string; backendName: string; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index d812275d7103b..542d240b1cd1b 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -8,6 +8,7 @@ import { getServiceMapServiceNodeInfo } from './get_service_map_service_node_info'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import * as getErrorRateModule from '../transaction_groups/get_error_rate'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; describe('getServiceMapServiceNodeInfo', () => { describe('with no results', () => { @@ -77,6 +78,7 @@ describe('getServiceMapServiceNodeInfo', () => { setup, serviceName, searchAggregatedTransactions: false, + environment: ENVIRONMENT_ALL.value, }); expect(result).toEqual({ diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index f9af5d227a867..f74c68be1c8cc 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -36,13 +36,13 @@ import { getErrorRate } from '../transaction_groups/get_error_rate'; interface Options { setup: Setup & SetupTimeRange; - environment?: string; + environment: string; serviceName: string; searchAggregatedTransactions: boolean; } interface TaskParameters { - environment?: string; + environment: string; filter: ESFilter[]; searchAggregatedTransactions: boolean; minutes: number; @@ -103,7 +103,7 @@ async function getErrorStats({ }: { setup: Options['setup']; serviceName: string; - environment?: string; + environment: string; searchAggregatedTransactions: boolean; }) { return withApmSpan('get_error_rate_for_service_map_node', async () => { @@ -115,6 +115,7 @@ async function getErrorStats({ searchAggregatedTransactions, start, end, + kuery: '', }); return { avgErrorRate: noHits ? null : average }; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 5845ba6f5f1a5..96889857de5f7 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -28,7 +28,7 @@ export async function getTraceSampleIds({ setup, }: { serviceName?: string; - environment?: string; + environment: string; setup: Setup & SetupTimeRange; }) { const { start, end, apmEventClient, config } = setup; diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index 97c553f344205..4eb6abddd81a6 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -20,14 +20,21 @@ const getServiceNodes = async ({ kuery, setup, serviceName, + environment, }: { - kuery?: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; + environment: string; }) => { const { apmEventClient } = setup; - const projection = getServiceNodesProjection({ kuery, setup, serviceName }); + const projection = getServiceNodesProjection({ + kuery, + setup, + serviceName, + environment, + }); const params = mergeProjection(projection, { body: { diff --git a/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts b/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts index 8fdb090ed8f6d..85f4b67fbe380 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts @@ -12,6 +12,7 @@ import { } from '../../utils/test_helpers'; import { getServiceNodeMetadata } from '../services/get_service_node_metadata'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; describe('service node queries', () => { let mock: SearchParamsMock; @@ -22,7 +23,12 @@ describe('service node queries', () => { it('fetches services nodes', async () => { mock = await inspectSearchParams((setup) => - getServiceNodes({ setup, serviceName: 'foo' }) + getServiceNodes({ + setup, + serviceName: 'foo', + kuery: '', + environment: ENVIRONMENT_ALL.value, + }) ); expect(mock.params).toMatchSnapshot(); @@ -34,6 +40,7 @@ describe('service node queries', () => { setup, serviceName: 'foo', serviceNodeName: 'bar', + kuery: '', }) ); @@ -46,6 +53,7 @@ describe('service node queries', () => { setup, serviceName: 'foo', serviceNodeName: SERVICE_NODE_NAME_MISSING, + kuery: '', }) ); diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 7a36817dfc458..7439da9197667 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -27,7 +27,7 @@ export async function getDerivedServiceAnnotations({ searchAggregatedTransactions, }: { serviceName: string; - environment?: string; + environment: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }) { diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 1e8b528142936..e811031fdddd8 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -31,7 +31,7 @@ export function getStoredAnnotations({ }: { setup: Setup & SetupTimeRange; serviceName: string; - environment?: string; + environment: string; client: ElasticsearchClient; annotationsClient: ScopedAnnotationsClient; logger: Logger; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.ts index 3f60f5d47af16..1c4689f4ff8d2 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.ts @@ -21,7 +21,7 @@ export async function getServiceAnnotations({ logger, }: { serviceName: string; - environment?: string; + environment: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; annotationsClient?: ScopedAnnotationsClient; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts index 4cbc62d87eff6..8981f8b516d97 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts @@ -30,7 +30,7 @@ export async function getServiceAlerts({ start: number; end: number; serviceName: string; - environment?: string; + environment: string; transactionType: string; }) { const response = await ruleDataClient.getReader().search({ diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies.ts index 1b3ffcab3f159..d4472d495230a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies.ts @@ -25,7 +25,7 @@ export async function getServiceDependencies({ end: number; serviceName: string; numBuckets: number; - environment?: string; + environment: string; offset?: string; }) { const statsItems = await getConnectionStats({ diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies_breakdown.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies_breakdown.ts index 8ebf6b7e017d4..c0e1af40d98db 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies_breakdown.ts @@ -24,8 +24,8 @@ export async function getServiceDependenciesBreakdown({ start: number; end: number; serviceName: string; - environment?: string; - kuery?: string; + environment: string; + kuery: string; }) { const items = await getConnectionStats({ setup, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts index d277967dad774..237721dd8bd62 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts @@ -29,13 +29,13 @@ export async function getServiceErrorGroupDetailedStatistics({ start, end, }: { - kuery?: string; + kuery: string; serviceName: string; setup: Setup; numBuckets: number; transactionType: string; groupIds: string[]; - environment?: string; + environment: string; start: number; end: number; }): Promise> { @@ -117,13 +117,13 @@ export async function getServiceErrorGroupPeriods({ comparisonStart, comparisonEnd, }: { - kuery?: string; + kuery: string; serviceName: string; setup: Setup & SetupTimeRange; numBuckets: number; transactionType: string; groupIds: string[]; - environment?: string; + environment: string; comparisonStart?: number; comparisonEnd?: number; }) { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts index 0d50e14fc89b0..cbeb720ea8d1b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_main_statistics.ts @@ -25,11 +25,11 @@ export async function getServiceErrorGroupMainStatistics({ transactionType, environment, }: { - kuery?: string; + kuery: string; serviceName: string; setup: Setup & SetupTimeRange; transactionType: string; - environment?: string; + environment: string; }) { const { apmEventClient, start, end } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts index a58539da722c3..fc4dc44b259f6 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -39,8 +39,8 @@ export async function getServiceErrorGroups({ sortField, transactionType, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; setup: Setup & SetupTimeRange; size: number; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts index 79ecab45c75b3..b6621d590b17d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts @@ -23,9 +23,9 @@ export const getServiceInfrastructure = async ({ environment, setup, }: { - kuery?: string; + kuery: string; serviceName: string; - environment?: string; + environment: string; setup: Setup & SetupTimeRange; }) => { const { apmEventClient, start, end } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts index fc91efbc7c6e6..b806dd765f0ef 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts @@ -16,8 +16,8 @@ import { getServiceInstancesSystemMetricStatistics } from './get_service_instanc import { getServiceInstancesTransactionStatistics } from './get_service_instances_transaction_statistics'; interface ServiceInstanceDetailedStatisticsParams { - environment?: string; - kuery?: string; + environment: string; + kuery: string; latencyAggregationType: LatencyAggregationType; setup: Setup; serviceName: string; @@ -75,8 +75,8 @@ export async function getServiceInstancesDetailedStatisticsPeriods({ comparisonStart, comparisonEnd, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; latencyAggregationType: LatencyAggregationType; setup: Setup & SetupTimeRange; serviceName: string; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts index 48209d98e86ce..61e89ed8ca5e3 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_system_metric_statistics.ts @@ -62,8 +62,8 @@ export async function getServiceInstancesSystemMetricStatistics< end: number; numBuckets?: number; serviceNodeIds?: string[]; - environment?: string; - kuery?: string; + environment: string; + kuery: string; size?: number; isComparisonSearch: T; }): Promise>> { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts index d0f58ee8be31f..a51f4c4e0fb7d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instances_transaction_statistics.ts @@ -72,8 +72,8 @@ export async function getServiceInstancesTransactionStatistics< end: number; isComparisonSearch: T; serviceNodeIds?: string[]; - environment?: string; - kuery?: string; + environment: string; + kuery: string; size?: number; numBuckets?: number; }): Promise>> { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/main_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/main_statistics.ts index 8bfa67f8c6247..9ca3d284bcc1a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/main_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/main_statistics.ts @@ -13,8 +13,8 @@ import { getServiceInstancesSystemMetricStatistics } from './get_service_instanc import { getServiceInstancesTransactionStatistics } from './get_service_instances_transaction_statistics'; interface ServiceInstanceMainStatisticsParams { - environment?: string; - kuery?: string; + environment: string; + kuery: string; latencyAggregationType: LatencyAggregationType; setup: Setup & SetupTimeRange; serviceName: string; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index 0f0c174179052..9a3dd1cc443b2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -13,6 +13,7 @@ import { import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; import { mergeProjection } from '../../projections/util/merge_projection'; import { getServiceNodesProjection } from '../../projections/service_nodes'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; export async function getServiceNodeMetadata({ kuery, @@ -20,7 +21,7 @@ export async function getServiceNodeMetadata({ serviceNodeName, setup, }: { - kuery?: string; + kuery: string; serviceName: string; serviceNodeName: string; setup: Setup & SetupTimeRange; @@ -33,6 +34,7 @@ export async function getServiceNodeMetadata({ setup, serviceName, serviceNodeName, + environment: ENVIRONMENT_ALL.value, }), { body: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts index 9d5b173a1c7e2..d8a28a127499f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts @@ -44,8 +44,8 @@ export async function getServiceTransactionGroupDetailedStatistics({ start, end, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; transactionNames: string[]; setup: Setup; @@ -202,8 +202,8 @@ export async function getServiceTransactionGroupDetailedStatisticsPeriods({ latencyAggregationType: LatencyAggregationType; comparisonStart?: number; comparisonEnd?: number; - environment?: string; - kuery?: string; + environment: string; + kuery: string; }) { const { start, end } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts index c8ae36946a2f3..d96c2cfae8f81 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts @@ -44,8 +44,8 @@ export async function getServiceTransactionGroups({ transactionType, latencyAggregationType, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts index 6fc868b0f0a4e..b5d29e1e22fb8 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts @@ -11,7 +11,7 @@ import { getServiceAnomalies } from '../../service_map/get_service_anomalies'; import { ServicesItemsSetup } from './get_services_items'; interface AggregationParams { - environment?: string; + environment: string; setup: ServicesItemsSetup; searchAggregatedTransactions: boolean; } diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index 885d73b13bb93..7c4a7bc636c5a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -31,8 +31,8 @@ import { import { ServicesItemsSetup } from './get_services_items'; interface AggregationParams { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: ServicesItemsSetup; searchAggregatedTransactions: boolean; maxNumServices: number; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts index a3bab48646eb6..f37fd6e24f3cd 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts @@ -23,9 +23,9 @@ export async function getServicesFromMetricDocuments({ kuery, }: { setup: Setup & SetupTimeRange; - environment?: string; + environment: string; maxNumServices: number; - kuery?: string; + kuery: string; }) { const { apmEventClient, start, end } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 5c7579c779eaf..0f8405b241b5a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -24,8 +24,8 @@ export async function getServicesItems({ searchAggregatedTransactions, logger, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: ServicesItemsSetup; searchAggregatedTransactions: boolean; logger: Logger; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index e76eb3c28fddb..61cb4a28586d7 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -20,8 +20,8 @@ export async function getServices({ searchAggregatedTransactions, logger, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; logger: Logger; diff --git a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts index 90fa54f31eb34..4fb68fdd3f562 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts @@ -39,8 +39,8 @@ export async function getServiceTransactionDetailedStatistics({ offset, }: { serviceNames: string[]; - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; offset?: string; diff --git a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/index.ts b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/index.ts index d4ce90c79f4a6..bc89d8d139c67 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/index.ts @@ -18,8 +18,8 @@ export async function getServicesDetailedStatistics({ offset, }: { serviceNames: string[]; - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; offset?: string; diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index b7f265daeb465..e866918fc29bb 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -19,8 +19,8 @@ import { import { Setup } from '../helpers/setup_request'; interface Options { - environment?: string; - kuery?: string; + environment: string; + kuery: string; searchAggregatedTransactions: boolean; serviceName: string; setup: Setup; diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index 4434cc89d24f2..b21aadbde2afb 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -189,10 +189,10 @@ export async function getServiceProfilingStatistics({ valueType, logger, }: { - kuery?: string; + kuery: string; serviceName: string; setup: Setup & SetupTimeRange; - environment?: string; + environment: string; valueType: ProfilingValueType; logger: Logger; }) { diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts index 19de91a5a1055..adb91b30e3cc2 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts @@ -32,10 +32,10 @@ export async function getServiceProfilingTimeline({ environment, setup, }: { - kuery?: string; + kuery: string; serviceName: string; setup: Setup & SetupTimeRange; - environment?: string; + environment: string; }) { const { apmEventClient, start, end } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index 6adaca9c1a93d..a34382ddaf1fb 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -14,6 +14,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../utils/test_helpers'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; describe('services queries', () => { let mock: SearchParamsMock; @@ -52,6 +53,8 @@ describe('services queries', () => { setup, searchAggregatedTransactions: false, logger: {} as any, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index fc1e800bb0543..75c14b4ffc503 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -29,8 +29,8 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getAverages, getCounts, getSums } from './get_transaction_group_stats'; export interface TopTraceOptions { - environment?: string; - kuery?: string; + environment: string; + kuery: string; transactionName?: string; searchAggregatedTransactions: boolean; } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 5c33959399104..22891d929e9c0 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -39,8 +39,8 @@ export async function getErrorRate({ start, end, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; transactionType?: string; transactionName?: string; @@ -144,8 +144,8 @@ export async function getErrorRatePeriods({ comparisonStart, comparisonEnd, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; transactionType?: string; transactionName?: string; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts index 18f1c1bee50dc..e662a0d2d186f 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -10,6 +10,7 @@ import { SearchParamsMock, inspectSearchParams, } from '../../utils/test_helpers'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; describe('transaction group queries', () => { let mock: SearchParamsMock; @@ -23,6 +24,8 @@ describe('transaction group queries', () => { topTransactionGroupsFetcher( { searchAggregatedTransactions: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }, setup ) @@ -37,6 +40,8 @@ describe('transaction group queries', () => { topTransactionGroupsFetcher( { searchAggregatedTransactions: true, + environment: ENVIRONMENT_ALL.value, + kuery: '', }, setup ) diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index d8867c0dcc1e2..6c021fcada480 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -10,6 +10,7 @@ import * as constants from './constants'; import noDataResponse from './mock_responses/no_data.json'; import dataResponse from './mock_responses/data.json'; import { APMConfig } from '../../..'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; const mockIndices = { /* eslint-disable @typescript-eslint/naming-convention */ @@ -49,6 +50,8 @@ describe('getTransactionBreakdown', () => { serviceName: 'myServiceName', transactionType: 'request', setup: getMockSetup(noDataResponse), + environment: ENVIRONMENT_ALL.value, + kuery: '', }); expect(Object.keys(response.timeseries).length).toBe(0); @@ -59,6 +62,8 @@ describe('getTransactionBreakdown', () => { serviceName: 'myServiceName', transactionType: 'request', setup: getMockSetup(dataResponse), + environment: ENVIRONMENT_ALL.value, + kuery: '', }); const { timeseries } = response; @@ -87,6 +92,8 @@ describe('getTransactionBreakdown', () => { serviceName: 'myServiceName', transactionType: 'request', setup: getMockSetup(dataResponse), + environment: ENVIRONMENT_ALL.value, + kuery: '', }); const { timeseries } = response; @@ -99,6 +106,8 @@ describe('getTransactionBreakdown', () => { serviceName: 'myServiceName', transactionType: 'request', setup: getMockSetup(dataResponse), + environment: ENVIRONMENT_ALL.value, + kuery: '', }); const { timeseries } = response; diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 0b0dd6924c4aa..0912fdcf38ee1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -32,8 +32,8 @@ export async function getTransactionBreakdown({ transactionName, transactionType, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; transactionName?: string; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 62c284cd22053..e868f7de049f9 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -59,8 +59,8 @@ export async function getBuckets({ setup, searchAggregatedTransactions, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; transactionName: string; transactionType: string; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index 34b790a267c83..9c056bc506e92 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -27,8 +27,8 @@ export async function getDistributionMax({ setup, searchAggregatedTransactions, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; transactionName: string; transactionType: string; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts index ef92ce6edcafe..ef72f2434fde2 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts @@ -30,8 +30,8 @@ export async function getTransactionDistribution({ setup, searchAggregatedTransactions, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; transactionName: string; transactionType: string; diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts index bcd279c57f4a5..b0a9b27ba771b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts @@ -25,7 +25,7 @@ export async function getAnomalySeries({ setup, logger, }: { - environment?: string; + environment: string; serviceName: string; transactionType: string; transactionName?: string; diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index 183a754ea0809..c2fd718fbc95c 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -43,8 +43,8 @@ function searchLatency({ start, end, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; transactionType: string | undefined; transactionName: string | undefined; @@ -127,8 +127,8 @@ export async function getLatencyTimeseries({ start, end, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; serviceName: string; transactionType: string | undefined; transactionName: string | undefined; @@ -192,8 +192,8 @@ export async function getLatencyPeriods({ latencyAggregationType: LatencyAggregationType; comparisonStart?: number; comparisonEnd?: number; - kuery?: string; - environment?: string; + kuery: string; + environment: string; }) { const { start, end } = setup; const options = { diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index 638d87112ced2..b1d942a261387 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { inspectSearchParams, SearchParamsMock, @@ -26,6 +27,8 @@ describe('transaction queries', () => { serviceName: 'foo', transactionType: 'bar', setup, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); @@ -39,6 +42,8 @@ describe('transaction queries', () => { transactionType: 'bar', transactionName: 'baz', setup, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); @@ -55,6 +60,8 @@ describe('transaction queries', () => { transactionId: 'quz', setup, searchAggregatedTransactions: false, + environment: ENVIRONMENT_ALL.value, + kuery: '', }) ); diff --git a/x-pack/plugins/apm/server/projections/errors.ts b/x-pack/plugins/apm/server/projections/errors.ts index 8f32dd9e5be58..c977123d6d31b 100644 --- a/x-pack/plugins/apm/server/projections/errors.ts +++ b/x-pack/plugins/apm/server/projections/errors.ts @@ -20,8 +20,8 @@ export function getErrorGroupsProjection({ setup, serviceName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; }) { diff --git a/x-pack/plugins/apm/server/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts index 54bc498007cbb..21f6ee86e57b5 100644 --- a/x-pack/plugins/apm/server/projections/metrics.ts +++ b/x-pack/plugins/apm/server/projections/metrics.ts @@ -35,8 +35,8 @@ export function getMetricsProjection({ serviceName, serviceNodeName, }: { - environment?: string; - kuery?: string; + environment: string; + kuery: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; diff --git a/x-pack/plugins/apm/server/projections/service_nodes.ts b/x-pack/plugins/apm/server/projections/service_nodes.ts index 932309d9875c2..1e6a5b0e01113 100644 --- a/x-pack/plugins/apm/server/projections/service_nodes.ts +++ b/x-pack/plugins/apm/server/projections/service_nodes.ts @@ -11,22 +11,25 @@ import { mergeProjection } from './util/merge_projection'; import { getMetricsProjection } from './metrics'; export function getServiceNodesProjection({ - kuery, setup, serviceName, serviceNodeName, + environment, + kuery, }: { - kuery?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; + environment: string; + kuery: string; }) { return mergeProjection( getMetricsProjection({ - kuery, setup, serviceName, serviceNodeName, + environment, + kuery, }), { body: { diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts index 52992f16dac85..6709c32bea3f9 100644 --- a/x-pack/plugins/apm/server/projections/services.ts +++ b/x-pack/plugins/apm/server/projections/services.ts @@ -16,7 +16,7 @@ export function getServicesProjection({ setup, searchAggregatedTransactions, }: { - kuery?: string; + kuery: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; }) { diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts index 0175860e93d35..13879cb5fecb7 100644 --- a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts @@ -12,7 +12,7 @@ import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_prev import { setupRequest } from '../../lib/helpers/setup_request'; import { createApmServerRoute } from '../create_apm_server_route'; import { createApmServerRouteRepository } from '../create_apm_server_route_repository'; -import { rangeRt } from '../default_api_types'; +import { environmentRt, rangeRt } from '../default_api_types'; const alertParamsRt = t.intersection([ t.partial({ @@ -22,9 +22,9 @@ const alertParamsRt = t.intersection([ t.literal('99th'), ]), serviceName: t.string, - environment: t.string, transactionType: t.string, }), + environmentRt, rangeRt, ]); diff --git a/x-pack/plugins/apm/server/routes/backends.ts b/x-pack/plugins/apm/server/routes/backends.ts index c5c2b3cac2283..c22e196fda996 100644 --- a/x-pack/plugins/apm/server/routes/backends.ts +++ b/x-pack/plugins/apm/server/routes/backends.ts @@ -22,10 +22,15 @@ const topBackendsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/backends/top_backends', params: t.intersection([ t.type({ - query: t.intersection([rangeRt, t.type({ numBuckets: toNumberRt })]), + query: t.intersection([ + rangeRt, + environmentRt, + kueryRt, + t.type({ numBuckets: toNumberRt }), + ]), }), t.partial({ - query: t.intersection([environmentRt, offsetRt, kueryRt]), + query: offsetRt, }), ]), options: { diff --git a/x-pack/plugins/apm/server/routes/default_api_types.ts b/x-pack/plugins/apm/server/routes/default_api_types.ts index 16de83687eb7c..5622b12e1b099 100644 --- a/x-pack/plugins/apm/server/routes/default_api_types.ts +++ b/x-pack/plugins/apm/server/routes/default_api_types.ts @@ -8,6 +8,8 @@ import * as t from 'io-ts'; import { isoToEpochRt } from '@kbn/io-ts-utils'; +export { environmentRt } from '../../common/environment_rt'; + export const rangeRt = t.type({ start: isoToEpochRt, end: isoToEpochRt, @@ -20,6 +22,4 @@ export const comparisonRangeRt = t.partial({ comparisonEnd: isoToEpochRt, }); -export const environmentRt = t.partial({ environment: t.string }); - -export const kueryRt = t.partial({ kuery: t.string }); +export const kueryRt = t.type({ kuery: t.string }); diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts index e06fbdf7fb6d4..b9d44de681572 100644 --- a/x-pack/plugins/apm/server/routes/environments.ts +++ b/x-pack/plugins/apm/server/routes/environments.ts @@ -28,9 +28,13 @@ const environmentsRoute = createApmServerRoute({ const setup = await setupRequest(resources); const { params } = resources; const { serviceName } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }); const environments = await getEnvironments({ setup, diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index c2e3d0e81ce0a..838bc3a1f5c68 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -35,9 +35,13 @@ const observabilityOverviewRoute = createApmServerRoute({ const setup = await setupRequest(resources); const { bucketSize } = resources.params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }); return withApmSpan('observability_overview', async () => { const [serviceCount, transactionPerMinute] = await Promise.all([ diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 023753932d21f..2aacecf4a23de 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -50,9 +50,13 @@ const serviceMapRoute = createApmServerRoute({ query: { serviceName, environment }, } = params; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }); return getServiceMap({ setup, serviceName, @@ -88,9 +92,13 @@ const serviceMapServiceNodeRoute = createApmServerRoute({ query: { environment }, } = params; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }); return getServiceMapServiceNodeInfo({ environment, diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts index a2eb12662cbca..af5d8b7591d5f 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes.ts +++ b/x-pack/plugins/apm/server/routes/service_nodes.ts @@ -11,6 +11,7 @@ import { createApmServerRoute } from './create_apm_server_route'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceNodes } from '../lib/service_nodes'; import { rangeRt, kueryRt } from './default_api_types'; +import { environmentRt } from '../../common/environment_rt'; const serviceNodesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services/{serviceName}/serviceNodes', @@ -18,16 +19,21 @@ const serviceNodesRoute = createApmServerRoute({ path: t.type({ serviceName: t.string, }), - query: t.intersection([kueryRt, rangeRt]), + query: t.intersection([kueryRt, rangeRt, environmentRt]), }), options: { tags: ['access:apm'] }, handler: async (resources) => { const setup = await setupRequest(resources); const { params } = resources; const { serviceName } = params.path; - const { kuery } = params.query; + const { kuery, environment } = params.query; - const serviceNodes = await getServiceNodes({ kuery, setup, serviceName }); + const serviceNodes = await getServiceNodes({ + kuery, + setup, + serviceName, + environment, + }); return { serviceNodes }; }, }); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 10d5bc5e3abdb..b4d185fecf5e2 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -120,9 +120,13 @@ const serviceMetadataDetailsRoute = createApmServerRoute({ const { params } = resources; const { serviceName } = params.path; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }); return getServiceMetadataDetails({ serviceName, @@ -144,9 +148,13 @@ const serviceMetadataIconsRoute = createApmServerRoute({ const { params } = resources; const { serviceName } = params.path; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }); return getServiceMetadataIcons({ serviceName, @@ -169,9 +177,13 @@ const serviceAgentNameRoute = createApmServerRoute({ const setup = await setupRequest(resources); const { params } = resources; const { serviceName } = params.path; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }); return getServiceAgentName({ serviceName, @@ -198,9 +210,13 @@ const serviceTransactionTypesRoute = createApmServerRoute({ return getServiceTransactionTypes({ serviceName, setup, - searchAggregatedTransactions: await getSearchAggregatedTransactions( - setup - ), + searchAggregatedTransactions: await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }), }); }, }); @@ -248,17 +264,22 @@ const serviceAnnotationsRoute = createApmServerRoute({ const { observability } = plugins; - const [ - annotationsClient, - searchAggregatedTransactions, - ] = await Promise.all([ - observability - ? withApmSpan('get_scoped_annotations_client', () => - observability.setup.getScopedAnnotationsClient(context, request) - ) - : undefined, - getSearchAggregatedTransactions(setup), - ]); + const [annotationsClient, searchAggregatedTransactions] = await Promise.all( + [ + observability + ? withApmSpan('get_scoped_annotations_client', () => + observability.setup.getScopedAnnotationsClient(context, request) + ) + : undefined, + getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + start: setup.start, + end: setup.end, + kuery: '', + }), + ] + ); return getServiceAnnotations({ environment, diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index f50770cb5ded7..71413cc757538 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -243,9 +243,11 @@ const listAgentConfigurationServicesRoute = createApmServerRoute({ options: { tags: ['access:apm'] }, handler: async (resources) => { const setup = await setupRequest(resources); - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + kuery: '', + }); const serviceNames = await getServiceNames({ setup, searchAggregatedTransactions, @@ -267,9 +269,11 @@ const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({ const { params } = resources; const { serviceName } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + kuery: '', + }); const environments = await getEnvironments({ serviceName, diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 98467e1a4a0dd..d15f232411810 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -87,9 +87,11 @@ const anomalyDetectionEnvironmentsRoute = createApmServerRoute({ handler: async (resources) => { const setup = await setupRequest(resources); - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + kuery: '', + }); const environments = await getAllEnvironments({ setup, diff --git a/x-pack/plugins/observability/server/utils/queries.ts b/x-pack/plugins/observability/server/utils/queries.ts index ae4b1ed2ecd4e..921405bb0de8c 100644 --- a/x-pack/plugins/observability/server/utils/queries.ts +++ b/x-pack/plugins/observability/server/utils/queries.ts @@ -26,7 +26,7 @@ export function rangeQuery( ]; } -export function kqlQuery(kql?: string): estypes.QueryDslQueryContainer[] { +export function kqlQuery(kql: string): estypes.QueryDslQueryContainer[] { if (!kql) { return []; } diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts index 3712b49ce1696..f12256a33ef05 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts @@ -24,6 +24,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, serviceName: 'opbeans-java', transactionType: 'request' as string | undefined, + environment: 'ENVIRONMENT_ALL', }, }, }); diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts index 0f203a03b7b70..12d71530ecce1 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts @@ -23,6 +23,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: range.start, end: range.end, fieldNames: 'user_agent.name,user_agent.os.name,url.original', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }); registry.when( diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts index 206da2968b4c1..a4e4077a17483 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts @@ -22,6 +22,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { query: { start: range.start, end: range.end, + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }); diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts index bbb2097f63015..e41a830735a89 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts @@ -30,6 +30,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: '2020', end: '2021', percentileThreshold: 95, + kuery: '', }, }; diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts index 214cc363a3239..cfbe63e976655 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts @@ -22,6 +22,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { query: { start: range.start, end: range.end, + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }); diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts index a002996c83174..fc56615b3b13a 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts @@ -26,6 +26,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { fieldNames: 'user_agent.name,user_agent.os.name,url.original', maxLatency: 3581640.00000003, distributionInterval: 238776, + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }); registry.when( diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.ts b/x-pack/test/apm_api_integration/tests/feature_controls.ts index 2f24e947d053c..589fba8561ae6 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.ts @@ -44,44 +44,50 @@ export default function featureControlsTests({ getService }: FtrProviderContext) { // this doubles as a smoke test for the _inspect query parameter req: { - url: `/api/apm/services/foo/errors?start=${start}&end=${end}&_inspect=true`, + url: `/api/apm/services/foo/errors?start=${start}&end=${end}&_inspect=true&environment=ENVIRONMENT_ALL&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, }, { - req: { url: `/api/apm/services/foo/errors/bar?start=${start}&end=${end}` }, + req: { + url: `/api/apm/services/foo/errors/bar?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=`, + }, expectForbidden: expect403, expectResponse: expect200, }, { req: { - url: `/api/apm/services/foo/errors/distribution?start=${start}&end=${end}&groupId=bar`, + url: `/api/apm/services/foo/errors/distribution?start=${start}&end=${end}&groupId=bar&environment=ENVIRONMENT_ALL&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, }, { req: { - url: `/api/apm/services/foo/errors/distribution?start=${start}&end=${end}`, + url: `/api/apm/services/foo/errors/distribution?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, }, { req: { - url: `/api/apm/services/foo/metrics/charts?start=${start}&end=${end}&agentName=cool-agent`, + url: `/api/apm/services/foo/metrics/charts?start=${start}&end=${end}&agentName=cool-agent&environment=ENVIRONMENT_ALL&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, }, { - req: { url: `/api/apm/services?start=${start}&end=${end}` }, + req: { + url: `/api/apm/services?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=`, + }, expectForbidden: expect403, expectResponse: expect200, }, { - req: { url: `/api/apm/services/foo/agent_name?start=${start}&end=${end}` }, + req: { + url: `/api/apm/services/foo/agent_name?start=${start}&end=${end}`, + }, expectForbidden: expect403, expectResponse: expect200, }, @@ -91,32 +97,34 @@ export default function featureControlsTests({ getService }: FtrProviderContext) expectResponse: expect200, }, { - req: { url: `/api/apm/traces?start=${start}&end=${end}` }, + req: { url: `/api/apm/traces?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=` }, expectForbidden: expect403, expectResponse: expect200, }, { - req: { url: `/api/apm/traces/foo?start=${start}&end=${end}` }, + req: { + url: `/api/apm/traces/foo?start=${start}&end=${end}`, + }, expectForbidden: expect403, expectResponse: expect200, }, { req: { - url: `/api/apm/services/foo/transactions/charts/latency?environment=testing&start=${start}&end=${end}&transactionType=bar&latencyAggregationType=avg`, + url: `/api/apm/services/foo/transactions/charts/latency?environment=testing&start=${start}&end=${end}&transactionType=bar&latencyAggregationType=avg&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, }, { req: { - url: `/api/apm/services/foo/transactions/charts/latency?environment=testing&start=${start}&end=${end}&transactionType=bar&latencyAggregationType=avg&transactionName=baz`, + url: `/api/apm/services/foo/transactions/charts/latency?environment=testing&start=${start}&end=${end}&transactionType=bar&latencyAggregationType=avg&transactionName=baz&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, }, { req: { - url: `/api/apm/services/foo/transactions/charts/distribution?start=${start}&end=${end}&transactionType=bar&transactionName=baz`, + url: `/api/apm/services/foo/transactions/charts/distribution?start=${start}&end=${end}&transactionType=bar&transactionName=baz&environment=ENVIRONMENT_ALL&kuery=`, }, expectForbidden: expect403, expectResponse: expect200, diff --git a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts index 805ec3adeace5..891334e1c1db2 100644 --- a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts @@ -33,7 +33,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let chartsResponse: ChartResponse; before(async () => { chartsResponse = await supertest.get( - `/api/apm/services/opbeans-node/metrics/charts?start=${start}&end=${end}&agentName=${agentName}` + `/api/apm/services/opbeans-node/metrics/charts?start=${start}&end=${end}&agentName=${agentName}&kuery=&environment=ENVIRONMENT_ALL` ); }); it('contains CPU usage and System memory usage chart data', async () => { @@ -121,7 +121,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let chartsResponse: ChartResponse; before(async () => { chartsResponse = await supertest.get( - `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&agentName=${agentName}` + `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&agentName=${agentName}&environment=ENVIRONMENT_ALL&kuery=` ); }); @@ -410,7 +410,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const end = encodeURIComponent('2020-09-08T15:05:00.000Z'); const chartsResponse: ChartResponse = await supertest.get( - `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&agentName=${agentName}` + `/api/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&agentName=${agentName}&environment=ENVIRONMENT_ALL&kuery=` ); const systemMemoryUsageChart = chartsResponse.body.charts.find( diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts index 0461ac8e4821a..1520ecd644395 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts @@ -25,7 +25,9 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) registry.when('Service map with a basic license', { config: 'basic', archives: [] }, () => { it('is only be available to users with Platinum license (or higher)', async () => { - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + const response = await supertest.get( + `/api/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` + ); expect(response.status).to.be(403); @@ -38,7 +40,9 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) registry.when('Service map without data', { config: 'trial', archives: [] }, () => { describe('/api/apm/service-map', () => { it('returns an empty list', async () => { - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + const response = await supertest.get( + `/api/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` + ); expect(response.status).to.be(200); expect(response.body.elements.length).to.be(0); @@ -50,6 +54,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const q = querystring.stringify({ start: metadata.start, end: metadata.end, + environment: 'ENVIRONMENT_ALL', }); const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); @@ -74,6 +79,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const q = querystring.stringify({ start: metadata.start, end: metadata.end, + environment: 'ENVIRONMENT_ALL', }); const response = await supertest.get(`/api/apm/service-map/backend/postgres?${q}`); @@ -97,7 +103,9 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) let response: PromiseReturnType; before(async () => { - response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + response = await supertest.get( + `/api/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` + ); }); it('returns service map elements', () => { @@ -148,7 +156,9 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) describe('with ML data', () => { describe('with the default apm user', () => { before(async () => { - response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + response = await supertest.get( + `/api/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` + ); }); it('returns service map elements with anomaly stats', () => { @@ -234,7 +244,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) describe('with a user that does not have access to ML', () => { before(async () => { response = await supertestAsApmReadUserWithoutMlAccess.get( - `/api/apm/service-map?start=${start}&end=${end}` + `/api/apm/service-map?start=${start}&end=${end}&environment=ENVIRONMENT_ALL` ); }); @@ -277,6 +287,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const q = querystring.stringify({ start: metadata.start, end: metadata.end, + environment: 'ENVIRONMENT_ALL', }); const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); @@ -301,6 +312,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const q = querystring.stringify({ start: metadata.start, end: metadata.end, + environment: 'ENVIRONMENT_ALL', }); const response = await supertest.get(`/api/apm/service-map/backend/postgresql?${q}`); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts b/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts index 1f24fec7d77f7..1472c1f8c1a09 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/get_service_node_ids.ts @@ -30,6 +30,8 @@ export async function getServiceNodeIds({ start, end, transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts index e5a2e1c11bfc6..cdea0da2671bb 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts @@ -47,6 +47,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { serviceNodeIds: JSON.stringify( await getServiceNodeIds({ apmApiSupertest, start, end }) ), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -81,6 +83,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { numBuckets: 20, transactionType: 'request', serviceNodeIds: JSON.stringify(serviceNodeIds), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -135,6 +139,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts index 371e4d1910798..52ead7f2b7b81 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts @@ -39,6 +39,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); @@ -69,6 +71,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { start, end, transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); @@ -138,6 +142,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { start, end, transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); @@ -206,6 +212,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); diff --git a/x-pack/test/apm_api_integration/tests/services/annotations.ts b/x-pack/test/apm_api_integration/tests/services/annotations.ts index a91fb59ce998c..0a885301643c6 100644 --- a/x-pack/test/apm_api_integration/tests/services/annotations.ts +++ b/x-pack/test/apm_api_integration/tests/services/annotations.ts @@ -279,7 +279,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { }; const response = await request({ - url: `/api/apm/services/${serviceName}/annotation/search?start=${range.start}&end=${range.end}`, + url: `/api/apm/services/${serviceName}/annotation/search?start=${range.start}&end=${range.end}&environment=ENVIRONMENT_ALL`, method: 'GET', }); @@ -312,7 +312,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { ).to.be(200); const response = await request({ - url: `/api/apm/services/${serviceName}/annotation/search?start=${range.start}&end=${range.end}`, + url: `/api/apm/services/${serviceName}/annotation/search?start=${range.start}&end=${range.end}&environment=ENVIRONMENT_ALL`, method: 'GET', }); @@ -340,7 +340,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { ).to.be(200); const responseFromEarlierRange = await request({ - url: `/api/apm/services/${serviceName}/annotation/search?start=${earlierRange.start}&end=${earlierRange.end}`, + url: `/api/apm/services/${serviceName}/annotation/search?start=${earlierRange.start}&end=${earlierRange.end}&environment=ENVIRONMENT_ALL`, method: 'GET', }); @@ -387,7 +387,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { }; const allEnvironmentsResponse = await request({ - url: `/api/apm/services/${serviceName}/annotation/search?start=${range.start}&end=${range.end}`, + url: `/api/apm/services/${serviceName}/annotation/search?start=${range.start}&end=${range.end}&environment=ENVIRONMENT_ALL`, method: 'GET', }); diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts index d8a4e3891b32b..d7eea2d24ddd3 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts @@ -41,6 +41,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { numBuckets: 20, transactionType: 'request', groupIds: JSON.stringify(groupIds), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -67,6 +69,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { numBuckets: 20, transactionType: 'request', groupIds: JSON.stringify(groupIds), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -100,6 +104,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { numBuckets: 20, transactionType: 'request', groupIds: JSON.stringify(['foo']), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -135,6 +141,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -179,6 +187,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts index 3f6edf1ccccfa..1dbd01cd9b4f7 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts @@ -33,6 +33,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { start, end, transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -59,6 +61,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, transactionType: 'request', environment: 'production', + kuery: '', }, }) ); diff --git a/x-pack/test/apm_api_integration/tests/services/get_error_group_ids.ts b/x-pack/test/apm_api_integration/tests/services/get_error_group_ids.ts index cdac09f9ef7dd..b2253750ae7f9 100644 --- a/x-pack/test/apm_api_integration/tests/services/get_error_group_ids.ts +++ b/x-pack/test/apm_api_integration/tests/services/get_error_group_ids.ts @@ -28,6 +28,8 @@ export async function getErrorGroupIds({ start, end, transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); diff --git a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.ts index eac0bef7c5de0..f19cb71018be0 100644 --- a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.ts @@ -31,7 +31,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { const response = await supertest.get( url.format({ pathname: `/api/apm/services/detailed_statistics`, - query: { start, end, serviceNames: JSON.stringify(serviceNames), offset: '1d' }, + query: { + start, + end, + serviceNames: JSON.stringify(serviceNames), + environment: 'ENVIRONMENT_ALL', + kuery: '', + offset: '1d', + }, }) ); expect(response.status).to.be(200); @@ -50,7 +57,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { const response = await supertest.get( url.format({ pathname: `/api/apm/services/detailed_statistics`, - query: { start, end, serviceNames: JSON.stringify(serviceNames) }, + query: { + start, + end, + serviceNames: JSON.stringify(serviceNames), + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, }) ); expect(response.status).to.be(200); @@ -95,7 +108,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { const response = await supertest.get( url.format({ pathname: `/api/apm/services/detailed_statistics`, - query: { start, end, serviceNames: JSON.stringify([]) }, + query: { + start, + end, + serviceNames: JSON.stringify([]), + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, }) ); expect(response.status).to.be(400); @@ -111,6 +130,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, serviceNames: JSON.stringify(serviceNames), environment: 'production', + kuery: '', }, }) ); @@ -126,6 +146,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { start, end, serviceNames: JSON.stringify(serviceNames), + environment: 'ENVIRONMENT_ALL', kuery: 'transaction.type : "invalid_transaction_type"', }, }) @@ -150,6 +171,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, serviceNames: JSON.stringify(serviceNames), offset: '15m', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.ts b/x-pack/test/apm_api_integration/tests/services/throughput.ts index 2ecb9055baee4..9134b13e18db1 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.ts @@ -35,6 +35,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: metadata.start, end: metadata.end, transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); @@ -62,6 +64,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: metadata.start, end: metadata.end, transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); @@ -115,6 +119,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { start: metadata.start, end: metadata.end, transactionType: 'request', + environment: 'ENVIRONMENT_ALL', }, }, }); @@ -151,6 +156,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end: metadata.end, comparisonStart: metadata.start, comparisonEnd: moment(metadata.start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }, }); diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.ts b/x-pack/test/apm_api_integration/tests/services/top_services.ts index df0c7c927780a..86d5db591a6ba 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.ts @@ -30,7 +30,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get(`/api/apm/services?start=${start}&end=${end}`); + const response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=` + ); expect(response.status).to.be(200); expect(response.body.hasHistoricalData).to.be(false); @@ -52,7 +54,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { let sortedItems: typeof response.body.items; before(async () => { - response = await supertest.get(`/api/apm/services?start=${start}&end=${end}`); + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=` + ); sortedItems = sortBy(response.body.items, 'serviceName'); }); @@ -195,9 +199,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { } const [unfilteredResponse, filteredResponse] = await Promise.all([ - supertest.get(`/api/apm/services?start=${start}&end=${end}`) as Promise, supertest.get( - `/api/apm/services?start=${start}&end=${end}&kuery=${encodeURIComponent( + `/api/apm/services?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=` + ) as Promise, + supertest.get( + `/api/apm/services?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=${encodeURIComponent( 'not (processor.event:transaction)' )}` ) as Promise, @@ -232,7 +238,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; before(async () => { - response = await supertest.get(`/api/apm/services?start=${start}&end=${end}`); + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=` + ); }); it('the response is successful', () => { @@ -277,7 +285,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { let response: PromiseReturnType; before(async () => { response = await supertestAsApmReadUserWithoutMlAccess.get( - `/api/apm/services?start=${start}&end=${end}` + `/api/apm/services?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=` ); }); diff --git a/x-pack/test/apm_api_integration/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/tests/traces/top_traces.ts index 1f617bbc3bf3e..29604bfc990df 100644 --- a/x-pack/test/apm_api_integration/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/tests/traces/top_traces.ts @@ -23,7 +23,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Top traces when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles empty state', async () => { - const response = await supertest.get(`/api/apm/traces?start=${start}&end=${end}`); + const response = await supertest.get( + `/api/apm/traces?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=` + ); expect(response.status).to.be(200); expect(response.body.items.length).to.be(0); @@ -36,7 +38,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { let response: any; before(async () => { - response = await supertest.get(`/api/apm/traces?start=${start}&end=${end}`); + response = await supertest.get( + `/api/apm/traces?start=${start}&end=${end}&environment=ENVIRONMENT_ALL&kuery=` + ); }); it('returns the correct status code', async () => { diff --git a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/breakdown.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/breakdown.snap index 8e26064f6314d..186d48d8d7487 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/breakdown.snap +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/breakdown.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM API tests basic apm_8.0.0 when data is loaded returns the transaction breakdown for a service 1`] = ` +exports[`APM API tests basic apm_8.0.0 Breakdown when data is loaded returns the transaction breakdown for a service 1`] = ` Object { "timeseries": Array [ Object { @@ -1019,7 +1019,7 @@ Object { } `; -exports[`APM API tests basic apm_8.0.0 when data is loaded returns the transaction breakdown for a transaction group 9`] = ` +exports[`APM API tests basic apm_8.0.0 Breakdown when data is loaded returns the transaction breakdown for a transaction group 9`] = ` Array [ Object { "x": 1627973400000, diff --git a/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts b/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts index bc04f597c364d..92fdbc3588e39 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts @@ -24,49 +24,52 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Breakdown when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { const response = await supertest.get( - `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}` + `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&environment=ENVIRONMENT_ALL&kuery=` ); expect(response.status).to.be(200); expect(response.body).to.eql({ timeseries: [] }); }); }); - registry.when('when data is loaded', { config: 'basic', archives: [archiveName] }, () => { - it('returns the transaction breakdown for a service', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}` - ); + registry.when( + 'Breakdown when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the transaction breakdown for a service', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&environment=ENVIRONMENT_ALL&kuery=` + ); - expect(response.status).to.be(200); - expectSnapshot(response.body).toMatch(); - }); - it('returns the transaction breakdown for a transaction group', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&transactionName=${transactionName}` - ); + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + it('returns the transaction breakdown for a transaction group', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&transactionName=${transactionName}&environment=ENVIRONMENT_ALL&kuery=` + ); - expect(response.status).to.be(200); + expect(response.status).to.be(200); - const { timeseries } = response.body; + const { timeseries } = response.body; - const numberOfSeries = timeseries.length; + const numberOfSeries = timeseries.length; - expectSnapshot(numberOfSeries).toMatchInline(`1`); + expectSnapshot(numberOfSeries).toMatchInline(`1`); - const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; + const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; - const nonNullDataPoints = data.filter((y: number | null) => y !== null); + const nonNullDataPoints = data.filter((y: number | null) => y !== null); - expectSnapshot(nonNullDataPoints.length).toMatchInline(`61`); + expectSnapshot(nonNullDataPoints.length).toMatchInline(`61`); - expectSnapshot( - data.slice(0, 5).map(({ x, y }: { x: number; y: number | null }) => { - return { - x: new Date(x ?? NaN).toISOString(), - y, - }; - }) - ).toMatchInline(` + expectSnapshot( + data.slice(0, 5).map(({ x, y }: { x: number; y: number | null }) => { + return { + x: new Date(x ?? NaN).toISOString(), + y, + }; + }) + ).toMatchInline(` Array [ Object { "x": "2021-08-03T06:50:00.000Z", @@ -91,22 +94,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { ] `); - expectSnapshot(title).toMatchInline(`"app"`); - expectSnapshot(color).toMatchInline(`"#54b399"`); - expectSnapshot(type).toMatchInline(`"areaStacked"`); - expectSnapshot(hideLegend).toMatchInline(`false`); - expectSnapshot(legendValue).toMatchInline(`"100%"`); - - expectSnapshot(data).toMatch(); - }); - it('returns the transaction breakdown sorted by name', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}` - ); - - expect(response.status).to.be(200); - expectSnapshot(response.body.timeseries.map((serie: { title: string }) => serie.title)) - .toMatchInline(` + expectSnapshot(title).toMatchInline(`"app"`); + expectSnapshot(color).toMatchInline(`"#54b399"`); + expectSnapshot(type).toMatchInline(`"areaStacked"`); + expectSnapshot(hideLegend).toMatchInline(`false`); + expectSnapshot(legendValue).toMatchInline(`"100%"`); + + expectSnapshot(data).toMatch(); + }); + it('returns the transaction breakdown sorted by name', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transaction/charts/breakdown?start=${start}&end=${end}&transactionType=${transactionType}&environment=ENVIRONMENT_ALL&kuery=` + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body.timeseries.map((serie: { title: string }) => serie.title)) + .toMatchInline(` Array [ "app", "http", @@ -114,6 +117,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { "redis", ] `); - }); - }); + }); + } + ); } diff --git a/x-pack/test/apm_api_integration/tests/transactions/distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/distribution.ts index 36af3c477b2fd..3c322a727d1f0 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/distribution.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/distribution.ts @@ -23,6 +23,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end: metadata.end, transactionName: 'APIRestController#stats', transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', })}`; registry.when( diff --git a/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts index 23819fc05a257..bb6b465c9927c 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts @@ -30,7 +30,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const response = await supertest.get( format({ pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', - query: { start, end, transactionType }, + query: { start, end, transactionType, environment: 'ENVIRONMENT_ALL', kuery: '' }, }) ); expect(response.status).to.be(200); @@ -52,6 +52,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -76,7 +78,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const response = await supertest.get( format({ pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', - query: { start, end, transactionType }, + query: { start, end, transactionType, environment: 'ENVIRONMENT_ALL', kuery: '' }, }) ); errorRateResponse = response.body; @@ -142,6 +144,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency.ts b/x-pack/test/apm_api_integration/tests/transactions/latency.ts index 9ba20fb987aca..7fa2c76dd54d8 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/latency.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/latency.ts @@ -69,6 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { latencyAggregationType: 'avg', transactionType: 'request', environment: 'testing', + kuery: '', }, }) ); @@ -101,6 +102,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { latencyAggregationType: 'avg', transactionType: 'request', environment: 'testing', + kuery: '', }, }) ); @@ -125,6 +127,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { latencyAggregationType: 'p95', transactionType: 'request', environment: 'testing', + kuery: '', }, }) ); @@ -149,6 +152,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { latencyAggregationType: 'p99', transactionType: 'request', environment: 'testing', + kuery: '', }, }) ); @@ -179,6 +183,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -219,6 +225,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { latencyAggregationType: 'avg', transactionType: 'request', environment: 'does-not-exist', + kuery: '', }, }) ); @@ -258,6 +265,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, latencyAggregationType: 'avg', transactionType, + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -279,6 +288,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { latencyAggregationType: 'avg', transactionType, environment: 'production', + kuery: '', }, }) ); @@ -318,6 +328,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { latencyAggregationType: 'avg', transactionType, environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts index bf0b4d3118904..3a97195ec587f 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts @@ -39,6 +39,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { latencyAggregationType: 'avg', transactionType: 'request', transactionNames: JSON.stringify(transactionNames), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -64,6 +66,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { transactionType: 'request', latencyAggregationType: 'avg', transactionNames: JSON.stringify(transactionNames), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -118,6 +122,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { transactionType: 'request', latencyAggregationType: 'p99', transactionNames: JSON.stringify(transactionNames), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -166,6 +172,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { transactionType: 'request', latencyAggregationType: 'avg', transactionNames: JSON.stringify(['foo']), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -190,6 +198,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, comparisonStart: start, comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.ts index 19cdcd5f0671a..d0672946ad019 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.ts @@ -34,6 +34,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, latencyAggregationType: 'avg', transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -59,6 +61,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, transactionType: 'request', latencyAggregationType: 'avg', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); @@ -131,6 +135,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { end, transactionType: 'request', latencyAggregationType: 'p99', + environment: 'ENVIRONMENT_ALL', + kuery: '', }, }) ); From a8b443329492ac3501bb0fa57d5a03a3b5d28956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= Date: Fri, 13 Aug 2021 09:50:43 -0400 Subject: [PATCH 48/91] [CTI] Fixes AlienVaultOTX counts on the Overview page (#108448) --- .../security_solution/common/cti/constants.ts | 18 +++++++-------- .../containers/overview_cti_links/helpers.ts | 22 +++++++++---------- .../use_cti_event_counts.ts | 3 +-- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 7b22e9036f566..5a22497597e29 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -60,14 +60,14 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { export const DEFAULT_EVENT_ENRICHMENT_FROM = 'now-30d'; export const DEFAULT_EVENT_ENRICHMENT_TO = 'now'; -export const CTI_DEFAULT_SOURCES = [ - 'Abuse URL', - 'Abuse Malware', - 'AlienVault OTX', - 'Anomali', - 'Malware Bazaar', - 'MISP', - 'Recorded Future', -]; +export const CTI_DATASET_KEY_MAP: { [key: string]: string } = { + 'Abuse URL': 'threatintel.abuseurl', + 'Abuse Malware': 'threatintel.abusemalware', + 'AlienVault OTX': 'threatintel.otx', + Anomali: 'threatintel.anomali', + 'Malware Bazaar': 'threatintel.malwarebazaar', + MISP: 'threatintel.misp', + 'Recorded Future': 'threatintel.recordedfuture', +}; export const DEFAULT_CTI_SOURCE_INDEX = ['filebeat-*']; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts index dbe5b07d9da3c..6f7953e78731a 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts @@ -6,16 +6,19 @@ */ import { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import { CTI_DEFAULT_SOURCES } from '../../../../common/cti/constants'; +import { CTI_DATASET_KEY_MAP } from '../../../../common/cti/constants'; export interface CtiListItem { path: string; - title: string; + title: CtiDatasetTitle; count: number; } -export const EMPTY_LIST_ITEMS: CtiListItem[] = CTI_DEFAULT_SOURCES.map((item) => ({ - title: item, +export type CtiDatasetTitle = keyof typeof CTI_DATASET_KEY_MAP; +export const ctiTitles = Object.keys(CTI_DATASET_KEY_MAP) as CtiDatasetTitle[]; + +export const EMPTY_LIST_ITEMS: CtiListItem[] = ctiTitles.map((title) => ({ + title, count: 0, path: '', })); @@ -37,7 +40,7 @@ export const OVERVIEW_DASHBOARD_LINK_TITLE = 'Overview'; export const getListItemsWithoutLinks = (eventCounts: EventCounts): CtiListItem[] => { return EMPTY_LIST_ITEMS.map((item) => ({ ...item, - count: eventCounts[item.title.replace(' ', '').toLowerCase()] ?? 0, + count: eventCounts[CTI_DATASET_KEY_MAP[item.title]] ?? 0, })); }; @@ -58,15 +61,12 @@ export const createLinkFromDashboardSO = ( : undefined; return { title, - count: - typeof title === 'string' - ? eventCountsByDataset[title.replace(' ', '').toLowerCase()] - : undefined, + count: typeof title === 'string' ? eventCountsByDataset[CTI_DATASET_KEY_MAP[title]] : undefined, path, }; }; -export const emptyEventCountsByDataset = CTI_DEFAULT_SOURCES.reduce((acc, item) => { - acc[item.toLowerCase().replace(' ', '')] = 0; +export const emptyEventCountsByDataset = Object.values(CTI_DATASET_KEY_MAP).reduce((acc, id) => { + acc[id] = 0; return acc; }, {} as { [key: string]: number }); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts index 65e79ac6b617d..c8076ab6a4484 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts @@ -11,7 +11,6 @@ import { emptyEventCountsByDataset } from './helpers'; import { CtiEnabledModuleProps } from '../../components/overview_cti_links/cti_enabled_module'; export const ID = 'ctiEventCountQuery'; -const PREFIX = 'threatintel.'; export const useCtiEventCounts = ({ deleteQuery, from, setQuery, to }: CtiEnabledModuleProps) => { const [isInitialLoading, setIsInitialLoading] = useState(true); @@ -23,7 +22,7 @@ export const useCtiEventCounts = ({ deleteQuery, from, setQuery, to }: CtiEnable data.reduce( (acc, item) => { if (item.y && item.g) { - const id = item.g.replace(PREFIX, ''); + const id = item.g; acc[id] += item.y; } return acc; From 5bfba1b014db6e6b23f25c36209201b07ca54380 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Fri, 13 Aug 2021 10:00:48 -0400 Subject: [PATCH 49/91] [App Search] Added a SitemapsTable to the Crawler view (#108405) --- .../components/sitemaps_table.test.tsx | 185 ++++++++++++++++++ .../crawler/components/sitemaps_table.tsx | 124 ++++++++++++ .../crawler/crawler_single_domain.tsx | 5 + .../crawler_single_domain_logic.test.ts | 30 +++ .../crawler/crawler_single_domain_logic.ts | 6 +- .../app_search/crawler_sitemaps.test.ts | 134 +++++++++++++ .../routes/app_search/crawler_sitemaps.ts | 77 ++++++++ .../server/routes/app_search/index.ts | 2 + 8 files changed, 562 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.tsx create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx new file mode 100644 index 0000000000000..8d7aa83cd2ec6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx @@ -0,0 +1,185 @@ +/* + * 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 { mockFlashMessageHelpers, setMockActions } from '../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiFieldText } from '@elastic/eui'; + +import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; + +import { mountWithIntl } from '../../../../test_helpers'; + +import { SitemapsTable } from './sitemaps_table'; + +describe('SitemapsTable', () => { + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; + const engineName = 'my-engine'; + const sitemaps = [ + { id: '1', url: 'http://www.example.com/sitemap.xml' }, + { id: '2', url: 'http://www.example.com/whatever/sitemaps.xml' }, + ]; + const domain = { + createdOn: '2018-01-01T00:00:00.000Z', + documentCount: 10, + id: '6113e1407a2f2e6f42489794', + url: 'https://www.elastic.co', + crawlRules: [], + entryPoints: [], + sitemaps, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(GenericEndpointInlineEditableTable).exists()).toBe(true); + }); + + describe('the first and only column in the table', () => { + it('shows the url of a sitemap', () => { + const sitemap = { id: '1', url: 'http://www.example.com/sitemap.xml' }; + + const wrapper = shallow( + + ); + + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + const column = shallow(
{columns[0].render(sitemap)}
); + expect(column.html()).toContain('http://www.example.com/sitemap.xml'); + }); + + it('can show the url of a sitemap as editable', () => { + const sitemap = { id: '1', url: 'http://www.example.com/sitemap.xml' }; + const onChange = jest.fn(); + + const wrapper = shallow( + + ); + + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + const column = shallow( +
+ {columns[0].editingRender(sitemap, onChange, { isInvalid: false, isLoading: false })} +
+ ); + + const textField = column.find(EuiFieldText); + expect(textField.props()).toEqual( + expect.objectContaining({ + value: 'http://www.example.com/sitemap.xml', + disabled: false, // It would be disabled if isLoading is true + isInvalid: false, + }) + ); + + textField.simulate('change', { target: { value: '/foo' } }); + expect(onChange).toHaveBeenCalledWith('/foo'); + }); + }); + + describe('routes', () => { + it('can calculate an update and delete route correctly', () => { + const wrapper = shallow( + + ); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const sitemap = { id: '1', url: '/whatever' }; + expect(table.prop('deleteRoute')(sitemap)).toEqual( + '/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/sitemaps/1' + ); + expect(table.prop('updateRoute')(sitemap)).toEqual( + '/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/sitemaps/1' + ); + }); + }); + + it('shows a no items message whem there are no sitemaps to show', () => { + const wrapper = shallow( + + ); + + const editNewItems = jest.fn(); + const table = wrapper.find(GenericEndpointInlineEditableTable); + const message = mountWithIntl(
{table.prop('noItemsMessage')!(editNewItems)}
); + expect(message.find(EuiEmptyPrompt).exists()).toBe(true); + }); + + describe('when a sitemap is added', () => { + it('should update the sitemaps for the current domain, and clear flash messages', () => { + const updateSitemaps = jest.fn(); + setMockActions({ + updateSitemaps, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const sitemapThatWasAdded = { id: '2', value: 'bar' }; + const updatedSitemaps = [ + { id: '1', value: 'foo' }, + { id: '2', value: 'bar' }, + ]; + table.prop('onAdd')(sitemapThatWasAdded, updatedSitemaps); + expect(updateSitemaps).toHaveBeenCalledWith(updatedSitemaps); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when a sitemap is updated', () => { + it('should update the sitemaps for the current domain, and clear flash messages', () => { + const updateSitemaps = jest.fn(); + setMockActions({ + updateSitemaps, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const sitemapThatWasUpdated = { id: '2', value: 'bar' }; + const updatedSitemaps = [ + { id: '1', value: 'foo' }, + { id: '2', value: 'baz' }, + ]; + table.prop('onUpdate')(sitemapThatWasUpdated, updatedSitemaps); + expect(updateSitemaps).toHaveBeenCalledWith(updatedSitemaps); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when a sitemap is deleted', () => { + it('should update the sitemaps for the current domain, clear flash messages, and show a success', () => { + const updateSitemaps = jest.fn(); + setMockActions({ + updateSitemaps, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const sitemapThatWasDeleted = { id: '2', value: 'bar' }; + const updatedSitemaps = [{ id: '1', value: 'foo' }]; + table.prop('onDelete')(sitemapThatWasDeleted, updatedSitemaps); + expect(updateSitemaps).toHaveBeenCalledWith(updatedSitemaps); + expect(clearFlashMessages).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.tsx new file mode 100644 index 0000000000000..eaa1526299fcd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.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 React from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton, EuiEmptyPrompt, EuiFieldText, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { clearFlashMessages, flashSuccessToast } from '../../../../shared/flash_messages'; +import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; +import { InlineEditableTableColumn } from '../../../../shared/tables/inline_editable_table/types'; +import { ItemWithAnID } from '../../../../shared/tables/types'; +import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic'; +import { CrawlerDomain, Sitemap } from '../types'; + +const ADD_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.sitemapsTable.addButtonLabel', + { defaultMessage: 'Add sitemap' } +); + +interface SitemapsTableProps { + domain: CrawlerDomain; + engineName: string; + items: Sitemap[]; +} + +export const SitemapsTable: React.FC = ({ domain, engineName, items }) => { + const { updateSitemaps } = useActions(CrawlerSingleDomainLogic); + const field = 'url'; + + const columns: Array> = [ + { + editingRender: (sitemap, onChange, { isInvalid, isLoading }) => ( + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + /> + ), + render: (sitemap) => {(sitemap as Sitemap)[field]}, + name: i18n.translate('xpack.enterpriseSearch.appSearch.crawler.sitemapsTable.urlTableHead', { + defaultMessage: 'URL', + }), + field, + }, + ]; + + const sitemapsRoute = `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}/sitemaps`; + const getSitemapRoute = (sitemap: Sitemap) => + `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}/sitemaps/${sitemap.id}`; + + return ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.sitemapsTable.description', { + defaultMessage: 'Specify sitemap URLs for the crawler on this domain.', + })} +

+ } + instanceId="SitemapsTable" + items={items} + canRemoveLastItem + noItemsMessage={(editNewItem) => ( + <> + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.sitemapsTable.emptyMessageTitle', + { + defaultMessage: 'There are no existing sitemaps.', + } + )} + + } + titleSize="s" + body={Add a sitemap to specify an entry point for the crawler.} + actions={{ADD_BUTTON_LABEL}} + /> + + )} + addRoute={sitemapsRoute} + deleteRoute={getSitemapRoute} + updateRoute={getSitemapRoute} + dataProperty="sitemaps" + onAdd={(_, newSitemaps) => { + updateSitemaps(newSitemaps as Sitemap[]); + clearFlashMessages(); + }} + onDelete={(_, newSitemaps) => { + updateSitemaps(newSitemaps as Sitemap[]); + clearFlashMessages(); + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.sitemapsTable.deleteSuccessToastMessage', + { + defaultMessage: 'The sitemap has been deleted.', + } + ) + ); + }} + onUpdate={(_, newSitemaps) => { + updateSitemaps(newSitemaps as Sitemap[]); + clearFlashMessages(); + }} + title={i18n.translate('xpack.enterpriseSearch.appSearch.crawler.sitemapsTable.title', { + defaultMessage: 'Sitemaps', + })} + disableReordering + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index da910ebc30726..464ecbe157c4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -23,6 +23,7 @@ import { CrawlerStatusIndicator } from './components/crawler_status_indicator/cr import { DeleteDomainPanel } from './components/delete_domain_panel'; import { EntryPointsTable } from './components/entry_points_table'; import { ManageCrawlsPopover } from './components/manage_crawls_popover/manage_crawls_popover'; +import { SitemapsTable } from './components/sitemaps_table'; import { CRAWLER_TITLE } from './constants'; import { CrawlerSingleDomainLogic } from './crawler_single_domain_logic'; @@ -59,6 +60,10 @@ export const CrawlerSingleDomain: React.FC = () => { + + + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index ead0c0ad91ced..60f3aca7794eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -81,6 +81,36 @@ describe('CrawlerSingleDomainLogic', () => { }); }); }); + + describe('updateSitemaps', () => { + beforeEach(() => { + mount({ + domain: { + id: '507f1f77bcf86cd799439011', + sitemaps: [], + }, + }); + + CrawlerSingleDomainLogic.actions.updateSitemaps([ + { + id: '1234', + url: 'http://www.example.com/sitemap.xml', + }, + ]); + }); + + it('should update the sitemaps on the domain', () => { + expect(CrawlerSingleDomainLogic.values.domain).toEqual({ + id: '507f1f77bcf86cd799439011', + sitemaps: [ + { + id: '1234', + url: 'http://www.example.com/sitemap.xml', + }, + ], + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts index 780cab45564bb..24830e9d727ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts @@ -14,7 +14,7 @@ import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_CRAWLER_PATH } from '../../routes'; import { EngineLogic, generateEnginePath } from '../engine'; -import { CrawlerDomain, EntryPoint } from './types'; +import { CrawlerDomain, EntryPoint, Sitemap } from './types'; import { crawlerDomainServerToClient, getDeleteDomainSuccessMessage } from './utils'; export interface CrawlerSingleDomainValues { @@ -27,6 +27,7 @@ interface CrawlerSingleDomainActions { fetchDomainData(domainId: string): { domainId: string }; onReceiveDomainData(domain: CrawlerDomain): { domain: CrawlerDomain }; updateEntryPoints(entryPoints: EntryPoint[]): { entryPoints: EntryPoint[] }; + updateSitemaps(entryPoints: Sitemap[]): { sitemaps: Sitemap[] }; } export const CrawlerSingleDomainLogic = kea< @@ -38,6 +39,7 @@ export const CrawlerSingleDomainLogic = kea< fetchDomainData: (domainId) => ({ domainId }), onReceiveDomainData: (domain) => ({ domain }), updateEntryPoints: (entryPoints) => ({ entryPoints }), + updateSitemaps: (sitemaps) => ({ sitemaps }), }, reducers: { dataLoading: [ @@ -52,6 +54,8 @@ export const CrawlerSingleDomainLogic = kea< onReceiveDomainData: (_, { domain }) => domain, updateEntryPoints: (currentDomain, { entryPoints }) => ({ ...currentDomain, entryPoints } as CrawlerDomain), + updateSitemaps: (currentDomain, { sitemaps }) => + ({ ...currentDomain, sitemaps } as CrawlerDomain), }, ], }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts new file mode 100644 index 0000000000000..21ff58e2d85ef --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts @@ -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 { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; + +import { registerCrawlerSitemapRoutes } from './crawler_sitemaps'; + +describe('crawler sitemap routes', () => { + describe('POST /api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps', + }); + + registerCrawlerSitemapRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234' }, + body: { + url: 'http://www.example.com/sitemaps.xml', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + describe('PUT /api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', + }); + + registerCrawlerSitemapRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234', sitemapId: '5678' }, + body: { + url: 'http://www.example.com/sitemaps.xml', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + describe('DELETE /api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', + }); + + registerCrawlerSitemapRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234', sitemapId: '5678' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts new file mode 100644 index 0000000000000..ab4c390243d37 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerCrawlerSitemapRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + }), + body: schema.object({ + url: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps', + params: { + respond_with: 'index', + }, + }) + ); + + router.put( + { + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + sitemapId: schema.string(), + }), + body: schema.object({ + url: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + params: { + respond_with: 'index', + }, + }) + ); + + router.delete( + { + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/sitemaps/{sitemapId}', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + sitemapId: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + params: { + respond_with: 'index', + }, + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index af5cc78f01e78..3f794421348d7 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -11,6 +11,7 @@ import { registerAnalyticsRoutes } from './analytics'; import { registerApiLogsRoutes } from './api_logs'; import { registerCrawlerRoutes } from './crawler'; import { registerCrawlerEntryPointRoutes } from './crawler_entry_points'; +import { registerCrawlerSitemapRoutes } from './crawler_sitemaps'; import { registerCredentialsRoutes } from './credentials'; import { registerCurationsRoutes } from './curations'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; @@ -46,4 +47,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerOnboardingRoutes(dependencies); registerCrawlerRoutes(dependencies); registerCrawlerEntryPointRoutes(dependencies); + registerCrawlerSitemapRoutes(dependencies); }; From 212b1898e65d06b4279977017116340cd522462b Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 13 Aug 2021 10:10:53 -0400 Subject: [PATCH 50/91] Revert "[Enterprise Search] Set up basic scaffolding for Cypress tests in Kibana (#108309)" (#108541) This reverts commit 4d7aa45e149029392f5f724ba99f1ffa5b6188f1. --- src/dev/typescript/projects.ts | 16 ---- x-pack/plugins/enterprise_search/README.md | 83 ------------------- x-pack/plugins/enterprise_search/cypress.sh | 18 ---- .../applications/app_search/cypress.json | 20 ----- .../cypress/integration/engines.spec.ts | 18 ---- .../app_search/cypress/support/commands.ts | 19 ----- .../app_search/cypress/tsconfig.json | 5 -- .../enterprise_search/cypress.json | 21 ----- .../cypress/integration/overview.spec.ts | 42 ---------- .../enterprise_search/cypress/tsconfig.json | 5 -- .../applications/shared/cypress/commands.ts | 35 -------- .../applications/shared/cypress/routes.ts | 10 --- .../applications/shared/cypress/tsconfig.json | 8 -- .../workplace_search/cypress.json | 20 ----- .../cypress/integration/overview.spec.ts | 18 ---- .../cypress/support/commands.ts | 19 ----- .../workplace_search/cypress/tsconfig.json | 5 -- .../plugins/enterprise_search/tsconfig.json | 1 - .../cypress.config.ts | 39 --------- 19 files changed, 402 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/cypress.sh delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/cypress.json delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/cypress/integration/engines.spec.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/cypress/support/commands.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/cypress/tsconfig.json delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress.json delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/integration/overview.spec.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/tsconfig.json delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/cypress/commands.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/cypress/routes.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/cypress/tsconfig.json delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.json delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/integration/overview.spec.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/support/commands.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/tsconfig.json delete mode 100644 x-pack/test/functional_enterprise_search/cypress.config.ts diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 80da8bf71727a..419d4f0854ecc 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -40,22 +40,6 @@ export const PROJECTS = [ createProject('x-pack/plugins/security_solution/cypress/tsconfig.json', { name: 'security_solution/cypress', }), - createProject( - 'x-pack/plugins/enterprise_search/public/applications/shared/cypress/tsconfig.json', - { name: 'enterprise_search/cypress' } - ), - createProject( - 'x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/tsconfig.json', - { name: 'enterprise_search/cypress' } - ), - createProject( - 'x-pack/plugins/enterprise_search/public/applications/app_search/cypress/tsconfig.json', - { name: 'enterprise_search/cypress' } - ), - createProject( - 'x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/tsconfig.json', - { name: 'enterprise_search/cypress' } - ), createProject('x-pack/plugins/osquery/cypress/tsconfig.json', { name: 'osquery/cypress', }), diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md index 5c8d767de3099..ca8ee68c42a34 100644 --- a/x-pack/plugins/enterprise_search/README.md +++ b/x-pack/plugins/enterprise_search/README.md @@ -66,89 +66,6 @@ sh jest.sh public/applications/shared/flash_messages/flash_messages_logic.test.t ### E2E tests -We currently have two testing libraries in which we run E2E tests: - -- [Cypress](#cypress-tests) - - Will contain the majority of our happy path E2E testing -- [Kibana's Functional Test Runner (FTR)](#kibana-ftr-tests) - - Contains basic tests that only run when the Enterprise Search host is not configured - - It's likely we will not continue to expand these tests, and might even trim some over time (to be replaced by Cypress) - -#### Cypress tests - -Documentation: https://docs.cypress.io/ - -Cypress tests can be run directly from the `x-pack/plugins/enterprise_search` folder. You can use our handy cypress.sh script to run specific product test suites: - -```bash -# Basic syntax -sh cypress.sh {run|open} {suite} - -# Examples -sh cypress.sh run overview # run Enterprise Search overview tests -sh cypress.sh open overview # open Enterprise Search overview tests - -sh cypress.sh run as # run App Search tests -sh cypress.sh open as # open App Search tests - -sh cypress.sh run ws # run Workplace Search tests -sh cypress.sh open ws # open Workplace Search tests - -# Overriding env variables -sh cypress.sh open as --env username=enterprise_search password=123 - -# Overriding config settings, e.g. changing the base URL to a dev path, or enabling video recording -sh cypress.sh open as --config baseUrl=http://localhost:5601/xyz video=true - -# Only run a single specific test file -sh cypress.sh run ws --spec '**/example.spec.ts' - -# Opt to run Chrome headlessly -sh cypress.sh run ws --headless -``` - -There are 3 ways you can spin up the required environments to run our Cypress tests: - -1. Running Cypress against local dev environments: - - Elasticsearch: - - Start a local instance, or use Kibana's `yarn es snapshot` command (with all configurations/versions required to run Enterprise Search locally) - - NOTE: We generally recommend a fresh instance (or blowing away your `data/` folder) to reduce false negatives due to custom user data - - Kibana: - - You **must** have `csp.strict: false` and `csp.warnLegacyBrowsers: false` set in your `kibana.dev.yml`. - - You should either start Kibana with `yarn start --no-base-path` or pass `--config baseUrl=http://localhost:5601/xyz` into your Cypress command. - - Enterprise Search: - - Nothing extra is required to run Cypress tests, only what is already needed to run Kibana/Enterprise Search locally. -2. Running Cypress against Kibana's functional test server: - - :information_source: While we won't use the runner, we can still make use of Kibana's functional test server to help us spin up Elasticsearch and Kibana instances. - - NOTE: We recommend stopping any other local dev processes, to reduce issues with memory/performance - - From the `x-pack/` project folder, run `node scripts/functional_tests_server --config test/functional_enterprise_search/cypress.config.ts` - - Kibana: - - You will need to pass `--config baseUrl=http://localhost:5620` into your Cypress command. - - Enterprise Search: - - :warning: TODO: We _currently_ do not have a way of spinning up Enterprise Search from Kibana's FTR - for now, you can use local Enterprise Search (pointed at the FTR's `http://localhost:9220` Elasticsearch host instance) -3. Running Cypress against Enterprise Search dockerized stack scripts - - :warning: This is for Enterprise Search devs only, as this requires access to our closed source Enterprise Search repo - - `stack_scripts/start-with-es-native-auth.sh --with-kibana` - - Note that the tradeoff of an easier one-command start experience is you will not be able to run Cypress tests against any local changes. - -##### Debugging - -Cypress can either run silently in a headless browser in the command line (`run` or `--headless` mode), which is the default mode used by CI, or opened interactively in an included app and the Chrome browser (`open` or `--headed --no-exit` mode). - -For debugging failures locally, we generally recommend using open mode, which allows you to run a single specific test suite, and makes browser dev tools available to you so you can pause and inspect DOM as needed. - -> :warning: Although this is more extra caution than a hard-and-fast rule, we generally recommend taking a break and not clicking or continuing to use the app while tests are running. This can eliminate or lower the possibility of hard-to-reproduce/intermittently flaky behavior and timeouts due to user interference. - -##### Artifacts - -All failed tests will output a screenshot to the `x-pack/plugins/enterprise_search/target/cypress/screenshots` folder. We strongly recommend starting there for debugging failed tests to inspect error messages and UI state at point of failure. - -To track what Cypress is doing while running tests, you can pass in `--config video=true` which will output screencaptures to a `videos/` folder for all tests (both successful and failing). This can potentially provide more context leading up to the failure point, if a static screenshot isn't providing enough information. - -> :information_source: We have videos turned off in our config to reduce test runtime, especially on CI, but suggest re-enabling it for any deep debugging. - -#### Kibana FTR tests - See [our functional test runner README](../../test/functional_enterprise_search). Our automated accessibility tests can be found in [x-pack/test/accessibility/apps](../../test/accessibility/apps/enterprise_search.ts). diff --git a/x-pack/plugins/enterprise_search/cypress.sh b/x-pack/plugins/enterprise_search/cypress.sh deleted file mode 100644 index 9dbdd81ab788f..0000000000000 --- a/x-pack/plugins/enterprise_search/cypress.sh +++ /dev/null @@ -1,18 +0,0 @@ -#! /bin/bash - -# Use either `cypress run` or `cypress open` - defaults to run -MODE="${1:-run}" - -# Choose which product folder to use, e.g. `yarn cypress open as` -PRODUCT="${2}" -# Provide helpful shorthands -if [ "$PRODUCT" == "as" ]; then PRODUCT='app_search'; fi -if [ "$PRODUCT" == "ws" ]; then PRODUCT='workplace_search'; fi -if [ "$PRODUCT" == "overview" ]; then PRODUCT='enterprise_search'; fi - -# Pass all remaining arguments (e.g., ...rest) from the 3rd arg onwards -# as an open-ended string. Appends onto to the end the Cypress command -# @see https://docs.cypress.io/guides/guides/command-line.html#Options -ARGS="${*:3}" - -../../../node_modules/.bin/cypress "$MODE" --project "public/applications/$PRODUCT" --browser chrome $ARGS diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.json b/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.json deleted file mode 100644 index 766aaf6df36ad..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "supportFile": "./cypress/support/commands.ts", - "pluginsFile": false, - "retries": { - "runMode": 2 - }, - "baseUrl": "http://localhost:5601", - "env": { - "username": "elastic", - "password": "changeme" - }, - "screenshotsFolder": "../../../target/cypress/screenshots", - "videosFolder": "../../../target/cypress/videos", - "defaultCommandTimeout": 120000, - "execTimeout": 120000, - "pageLoadTimeout": 180000, - "viewportWidth": 1600, - "viewportHeight": 1200, - "video": false -} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/integration/engines.spec.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/integration/engines.spec.ts deleted file mode 100644 index 5e651aab075c6..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/integration/engines.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { login } from '../support/commands'; - -context('Engines', () => { - beforeEach(() => { - login(); - }); - - it('renders', () => { - cy.contains('Engines'); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/support/commands.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/support/commands.ts deleted file mode 100644 index 50b5fcd179297..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/support/commands.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { login as baseLogin } from '../../../shared/cypress/commands'; -import { appSearchPath } from '../../../shared/cypress/routes'; - -interface Login { - path?: string; - username?: string; - password?: string; -} -export const login = ({ path = '/', ...args }: Login = {}) => { - baseLogin({ ...args }); - cy.visit(`${appSearchPath}${path}`); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/tsconfig.json b/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/tsconfig.json deleted file mode 100644 index ce9df3ca76c09..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/cypress/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../shared/cypress/tsconfig.json", - "references": [{ "path": "../../shared/cypress/tsconfig.json" }], - "include": ["./**/*"] -} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress.json b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress.json deleted file mode 100644 index 8ca8bdfd79a49..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "supportFile": false, - "pluginsFile": false, - "retries": { - "runMode": 2 - }, - "baseUrl": "http://localhost:5601", - "env": { - "username": "elastic", - "password": "changeme" - }, - "fixturesFolder": false, - "screenshotsFolder": "../../../target/cypress/screenshots", - "videosFolder": "../../../target/cypress/videos", - "defaultCommandTimeout": 120000, - "execTimeout": 120000, - "pageLoadTimeout": 180000, - "viewportWidth": 1600, - "viewportHeight": 1200, - "video": false -} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/integration/overview.spec.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/integration/overview.spec.ts deleted file mode 100644 index 10742ce987b7d..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/integration/overview.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { login } from '../../../shared/cypress/commands'; -import { overviewPath } from '../../../shared/cypress/routes'; - -context('Enterprise Search Overview', () => { - beforeEach(() => { - login(); - }); - - it('should contain product cards', () => { - cy.visit(overviewPath); - cy.contains('Welcome to Elastic Enterprise Search'); - - cy.get('[data-test-subj="appSearchProductCard"]') - .contains('Launch App Search') - .should('have.attr', 'href') - .and('match', /app_search/); - - cy.get('[data-test-subj="workplaceSearchProductCard"]') - .contains('Launch Workplace Search') - .should('have.attr', 'href') - .and('match', /workplace_search/); - }); - - it('should have a setup guide', () => { - // @see https://github.com/quasarframework/quasar/issues/2233#issuecomment-492975745 - // This only appears to occur for setup guides - I haven't (yet?) run into it on other pages - cy.on('uncaught:exception', (err) => { - if (err.message.includes('> ResizeObserver loop limit exceeded')) return false; - }); - - cy.visit(`${overviewPath}/setup_guide`); - cy.contains('Setup Guide'); - cy.contains('Add your Enterprise Search host URL to your Kibana configuration'); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/tsconfig.json b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/tsconfig.json deleted file mode 100644 index ce9df3ca76c09..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/cypress/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../shared/cypress/tsconfig.json", - "references": [{ "path": "../../shared/cypress/tsconfig.json" }], - "include": ["./**/*"] -} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cypress/commands.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cypress/commands.ts deleted file mode 100644 index 5f9738fae5064..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/cypress/commands.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Shared non-product-specific commands - */ - -/* - * Log in a user via XHR - * @see https://docs.cypress.io/guides/getting-started/testing-your-app#Logging-in - */ -interface Login { - username?: string; - password?: string; -} -export const login = ({ - username = Cypress.env('username'), - password = Cypress.env('password'), -}: Login = {}) => { - cy.request({ - method: 'POST', - url: '/internal/security/login', - headers: { 'kbn-xsrf': 'cypress' }, - body: { - providerType: 'basic', - providerName: 'basic', - currentURL: '/', - params: { username, password }, - }, - }); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cypress/routes.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cypress/routes.ts deleted file mode 100644 index b1a0aaba95661..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/cypress/routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const overviewPath = '/app/enterprise_search/overview'; -export const appSearchPath = '/app/enterprise_search/app_search'; -export const workplaceSearchPath = '/app/enterprise_search/workplace_search'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cypress/tsconfig.json b/x-pack/plugins/enterprise_search/public/applications/shared/cypress/tsconfig.json deleted file mode 100644 index 725a36f893fa9..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/cypress/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../../../../../tsconfig.base.json", - "exclude": [], - "include": ["./**/*"], - "compilerOptions": { - "types": ["cypress", "node"] - } -} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.json b/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.json deleted file mode 100644 index 766aaf6df36ad..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "supportFile": "./cypress/support/commands.ts", - "pluginsFile": false, - "retries": { - "runMode": 2 - }, - "baseUrl": "http://localhost:5601", - "env": { - "username": "elastic", - "password": "changeme" - }, - "screenshotsFolder": "../../../target/cypress/screenshots", - "videosFolder": "../../../target/cypress/videos", - "defaultCommandTimeout": 120000, - "execTimeout": 120000, - "pageLoadTimeout": 180000, - "viewportWidth": 1600, - "viewportHeight": 1200, - "video": false -} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/integration/overview.spec.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/integration/overview.spec.ts deleted file mode 100644 index 8ce6e4ebcfb05..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/integration/overview.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { login } from '../support/commands'; - -context('Overview', () => { - beforeEach(() => { - login(); - }); - - it('renders', () => { - cy.contains('Workplace Search'); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/support/commands.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/support/commands.ts deleted file mode 100644 index d91b73fd78c05..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/support/commands.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { login as baseLogin } from '../../../shared/cypress/commands'; -import { workplaceSearchPath } from '../../../shared/cypress/routes'; - -interface Login { - path?: string; - username?: string; - password?: string; -} -export const login = ({ path = '/', ...args }: Login = {}) => { - baseLogin({ ...args }); - cy.visit(`${workplaceSearchPath}${path}`); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/tsconfig.json b/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/tsconfig.json deleted file mode 100644 index ce9df3ca76c09..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../shared/cypress/tsconfig.json", - "references": [{ "path": "../../shared/cypress/tsconfig.json" }], - "include": ["./**/*"] -} diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index ce288f8b4b97d..481c4527d5977 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -6,7 +6,6 @@ "declaration": true, "declarationMap": true }, - "exclude": ["public/applications/**/cypress/**/*"], "include": [ "common/**/*", "public/**/*", diff --git a/x-pack/test/functional_enterprise_search/cypress.config.ts b/x-pack/test/functional_enterprise_search/cypress.config.ts deleted file mode 100644 index 9a6918ab0557d..0000000000000 --- a/x-pack/test/functional_enterprise_search/cypress.config.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -// TODO: If Kibana CI doesn't end up using this (e.g., uses Dockerized containers -// instead of the functional test server), we can opt to delete this file later. - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const baseConfig = await readConfigFile(require.resolve('./base_config')); - - return { - // default to the xpack functional config - ...baseConfig.getAll(), - - esTestCluster: { - ...baseConfig.get('esTestCluster'), - serverArgs: [ - ...baseConfig.get('esTestCluster.serverArgs'), - 'xpack.security.enabled=true', - 'xpack.security.authc.api_key.enabled=true', - ], - }, - - kbnTestServer: { - ...baseConfig.get('kbnTestServer'), - serverArgs: [ - ...baseConfig.get('kbnTestServer.serverArgs'), - '--csp.strict=false', - '--csp.warnLegacyBrowsers=false', - '--enterpriseSearch.host=http://localhost:3002', - ], - }, - }; -} From 7dc24e65d6fc059b0d86014ecc88a7812789dbd6 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 13 Aug 2021 15:24:51 +0100 Subject: [PATCH 51/91] chore(NA): upgrades bazel rules nodejs into v3.8.0 (#108471) --- WORKSPACE.bazel | 6 +++--- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index a3c0cd76250e0..384277822709c 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -10,15 +10,15 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch Node.js rules http_archive( name = "build_bazel_rules_nodejs", - sha256 = "8f5f192ba02319254aaf2cdcca00ec12eaafeb979a80a1e946773c520ae0a2c9", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.7.0/rules_nodejs-3.7.0.tar.gz"], + sha256 = "e79c08a488cc5ac40981987d862c7320cee8741122a2649e9b08e850b6f20442", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.8.0/rules_nodejs-3.8.0.tar.gz"], ) # Now that we have the rules let's import from them to complete the work load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install") # Assure we have at least a given rules_nodejs version -check_rules_nodejs_version(minimum_version_string = "3.7.0") +check_rules_nodejs_version(minimum_version_string = "3.8.0") # Setup the Node.js toolchain for the architectures we want to support # diff --git a/package.json b/package.json index 70c89c197eeb9..17beadebca91c 100644 --- a/package.json +++ b/package.json @@ -446,7 +446,7 @@ "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", "@bazel/ibazel": "^0.15.10", - "@bazel/typescript": "^3.7.0", + "@bazel/typescript": "^3.8.0", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", "@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana", diff --git a/yarn.lock b/yarn.lock index 5ff06955b63cb..39e70709bbf0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1227,10 +1227,10 @@ resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.15.10.tgz#cf0cff1aec6d8e7bb23e1fc618d09fbd39b7a13f" integrity sha512-0v+OwCQ6fsGFa50r6MXWbUkSGuWOoZ22K4pMSdtWiL5LKFIE4kfmMmtQS+M7/ICNwk2EIYob+NRreyi/DGUz5A== -"@bazel/typescript@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.7.0.tgz#a4d648a36f7ef4960c8a16222f853a4c285a522d" - integrity sha512-bkNHZaCWg4Jk+10wzhFDhB+RRZkfob/yydC4qRzUVxCDLPFICYgC0PWeLhf/ixEhVeHtS0Cmv74M+QziqKSdbw== +"@bazel/typescript@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.8.0.tgz#725d51a1c25e314a1d8cddb8b880ac05ba97acd4" + integrity sha512-4C1pLe4V7aidWqcPsWNqXFS7uHAB1nH5SUKG5uWoVv4JT9XhkNSvzzQIycMwXs2tZeCylX4KYNeNvfKrmkyFlw== dependencies: protobufjs "6.8.8" semver "5.6.0" From 4a1366ca524a195540705289e2e51435ffb023c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= Date: Fri, 13 Aug 2021 17:01:56 +0200 Subject: [PATCH 52/91] [APM] Change table SparkPlot content properties (#108516) --- .../service_overview_errors_table/get_column.tsx | 1 + .../public/components/shared/charts/spark_plot/index.tsx | 9 +++++++-- .../components/shared/dependencies_table/index.tsx | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx index cd2c70b0f10b7..ae1fc581f5fdc 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx @@ -62,6 +62,7 @@ export function getColumns({ return ; }, width: `${unit * 9}px`, + align: 'right', }, { field: 'occurrences', diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 6b93fe9605e42..6206965882243 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -73,13 +73,18 @@ export function SparkPlot({ const chartSize = { height: theme.eui.euiSizeL, - width: compact ? unit * 3 : unit * 4, + width: compact ? unit * 4 : unit * 5, }; const Sparkline = hasComparisonSeries ? LineSeries : AreaSeries; return ( - + {hasValidTimeseries(series) ? ( diff --git a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx index 0b8fb787c8d8a..9c5c6368f5758 100644 --- a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx @@ -82,6 +82,7 @@ export function DependenciesTable(props: Props) { render: (_, { currentStats, previousStats }) => { return ( Date: Fri, 13 Aug 2021 17:52:04 +0200 Subject: [PATCH 53/91] [ML] APM Latency Correlations: Improve log log chart loading behavior and axis ticks. (#108211) - Makes use of ChartContainer to improve the loading behavior of the log log chart to include a loading indicator. - Improves y axis ticks for the log log chart. Will set the max y domain to the next rounded value with one more digit, for example, if the max y value is 4567, the y domain will be extended to 10000 and 10000 being the top tick. This makes sure we'll always have a top tick, fixes a bug where with low number <10 we'd end up with just a low 1 tick. - Improves x axis ticks to support different time units. --- .../apm/common/utils/formatters/duration.ts | 13 +- .../app/correlations/correlations_chart.tsx | 186 +++++++++--------- .../correlations/ml_latency_correlations.tsx | 46 ++--- 3 files changed, 126 insertions(+), 119 deletions(-) diff --git a/x-pack/plugins/apm/common/utils/formatters/duration.ts b/x-pack/plugins/apm/common/utils/formatters/duration.ts index b060f1aa6e005..917521117af4e 100644 --- a/x-pack/plugins/apm/common/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/common/utils/formatters/duration.ts @@ -35,7 +35,7 @@ export type TimeFormatter = ( type TimeFormatterBuilder = (max: number) => TimeFormatter; -function getUnitLabelAndConvertedValue( +export function getUnitLabelAndConvertedValue( unitKey: DurationTimeUnit, value: number ) { @@ -122,14 +122,17 @@ function convertTo({ export const toMicroseconds = (value: number, timeUnit: TimeUnit) => moment.duration(value, timeUnit).asMilliseconds() * 1000; -function getDurationUnitKey(max: number): DurationTimeUnit { - if (max > toMicroseconds(10, 'hours')) { +export function getDurationUnitKey( + max: number, + threshold = 10 +): DurationTimeUnit { + if (max > toMicroseconds(threshold, 'hours')) { return 'hours'; } - if (max > toMicroseconds(10, 'minutes')) { + if (max > toMicroseconds(threshold, 'minutes')) { return 'minutes'; } - if (max > toMicroseconds(10, 'seconds')) { + if (max > toMicroseconds(threshold, 'seconds')) { return 'seconds'; } if (max > toMicroseconds(1, 'milliseconds')) { diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx index 50d5f28aa5d55..e3ff631ae1a6f 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx @@ -24,15 +24,20 @@ import { import euiVars from '@elastic/eui/dist/eui_theme_light.json'; -import { EuiSpacer } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; -import { getDurationFormatter } from '../../../../common/utils/formatters'; +import { + getDurationUnitKey, + getUnitLabelAndConvertedValue, +} from '../../../../common/utils/formatters'; -import { useTheme } from '../../../hooks/use_theme'; import { HistogramItem } from '../../../../common/search_strategies/correlations/types'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; + +import { ChartContainer } from '../../shared/charts/chart_container'; + const { euiColorMediumShade } = euiVars; const axisColor = euiColorMediumShade; @@ -70,18 +75,13 @@ const chartTheme: PartialTheme = { }, }; -// Log based axis cannot start a 0. Use a small positive number instead. -const yAxisDomain = { - min: 0.9, -}; - interface CorrelationsChartProps { field?: string; value?: string; histogram?: HistogramItem[]; markerValue: number; markerPercentile: number; - overallHistogram: HistogramItem[]; + overallHistogram?: HistogramItem[]; } const annotationsStyle = { @@ -133,7 +133,6 @@ export function CorrelationsChart({ }: CorrelationsChartProps) { const euiTheme = useTheme(); - if (!Array.isArray(overallHistogram)) return
; const annotationsDataValues: LineAnnotationDatum[] = [ { dataValue: markerValue, @@ -149,9 +148,14 @@ export function CorrelationsChart({ }, ]; - const xMax = Math.max(...overallHistogram.map((d) => d.key)) ?? 0; - - const durationFormatter = getDurationFormatter(xMax); + // This will create y axis ticks for 1, 10, 100, 1000 ... + const yMax = + Math.max(...(overallHistogram ?? []).map((d) => d.doc_count)) ?? 0; + const yTicks = Math.ceil(Math.log10(yMax)); + const yAxisDomain = { + min: 0.9, + max: Math.pow(10, yTicks), + }; const histogram = replaceHistogramDotsWithBars(originalHistogram); @@ -160,82 +164,86 @@ export function CorrelationsChart({ data-test-subj="apmCorrelationsChart" style={{ overflow: 'hidden', textOverflow: 'ellipsis' }} > - 0} + status={ + Array.isArray(overallHistogram) + ? FETCH_STATUS.SUCCESS + : FETCH_STATUS.LOADING + } > - - - durationFormatter(d).formatted} - /> - - d === 0 || Number.isInteger(Math.log10(d)) ? d : '' - } - /> - - {Array.isArray(histogram) && - field !== undefined && - value !== undefined && ( - - )} - - + + + + { + const unit = getDurationUnitKey(d, 1); + const converted = getUnitLabelAndConvertedValue(unit, d); + const convertedValueParts = converted.convertedValue.split('.'); + const convertedValue = + convertedValueParts.length === 2 && + convertedValueParts[1] === '0' + ? convertedValueParts[0] + : converted.convertedValue; + return `${convertedValue}${converted.unitLabel}`; + }} + /> + + + {Array.isArray(histogram) && + field !== undefined && + value !== undefined && ( + + )} + +
); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx index 7b9c8461221d6..bbd6648ccaf6e 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx @@ -349,33 +349,29 @@ export function MlLatencyCorrelations({ onClose }: Props) { )} - {overallHistogram !== undefined ? ( - <> - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.chartTitle', - { - defaultMessage: - 'Latency distribution for {name} (Log-Log Plot)', - values: { - name: transactionName ?? serviceName, - }, - } - )} -

-
- + +

+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.chartTitle', + { + defaultMessage: 'Latency distribution for {name} (Log-Log Plot)', + values: { + name: transactionName ?? serviceName, + }, + } + )} +

+
- - - ) : null} + + +
{histograms.length > 0 && selectedHistogram !== undefined && ( From fe3b7d61c8ef108124ce58e8f5e0b840328c8755 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 13 Aug 2021 17:55:38 +0200 Subject: [PATCH 54/91] [ML] Fix the Job audit messages service (#108526) * [ML] refactor to ts * [ML] fix types --- .../{message_levels.js => message_levels.ts} | 2 +- .../anomaly_detection_jobs/summary_job.ts | 4 +- .../job_audit_messages.d.ts | 33 ----- ...udit_messages.js => job_audit_messages.ts} | 131 ++++++++++++------ 4 files changed, 92 insertions(+), 78 deletions(-) rename x-pack/plugins/ml/common/constants/{message_levels.js => message_levels.ts} (96%) delete mode 100644 x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts rename x-pack/plugins/ml/server/models/job_audit_messages/{job_audit_messages.js => job_audit_messages.ts} (71%) diff --git a/x-pack/plugins/ml/common/constants/message_levels.js b/x-pack/plugins/ml/common/constants/message_levels.ts similarity index 96% rename from x-pack/plugins/ml/common/constants/message_levels.js rename to x-pack/plugins/ml/common/constants/message_levels.ts index 16269bbf755da..fd6cef75174ae 100644 --- a/x-pack/plugins/ml/common/constants/message_levels.js +++ b/x-pack/plugins/ml/common/constants/message_levels.ts @@ -10,4 +10,4 @@ export const MESSAGE_LEVEL = { INFO: 'info', SUCCESS: 'success', WARNING: 'warning', -}; +} as const; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index 28071f88da9d9..37dad58bfbd45 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -42,10 +42,10 @@ export interface MlSummaryJob { export interface AuditMessage { job_id: string; msgTime: number; - level: string; + level?: string; highestLevel: string; highestLevelText: string; - text: string; + text?: string; cleared?: boolean; } diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts deleted file mode 100644 index d3748163957db..0000000000000 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IScopedClusterClient } from 'kibana/server'; -import type { MlClient } from '../../lib/ml_client'; -import type { JobSavedObjectService } from '../../saved_objects'; -import { JobMessage } from '../../../common/types/audit_message'; - -export function isClearable(index?: string): boolean; - -export function jobAuditMessagesProvider( - client: IScopedClusterClient, - mlClient: MlClient -): { - getJobAuditMessages: ( - jobSavedObjectService: JobSavedObjectService, - options: { - jobId?: string; - from?: string; - start?: string; - end?: string; - } - ) => { messages: JobMessage[]; notificationIndices: string[] }; - getAuditMessagesSummary: (jobIds?: string[]) => any; - clearJobAuditMessages: ( - jobId: string, - notificationIndices: string[] - ) => { success: boolean; last_cleared: number }; -}; diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts similarity index 71% rename from x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js rename to x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts index 311df2ac418c0..98ed76319a0f7 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts @@ -5,12 +5,22 @@ * 2.0. */ +import moment from 'moment'; +import type { IScopedClusterClient } from 'kibana/server'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import type { estypes } from '@elastic/elasticsearch'; import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { MESSAGE_LEVEL } from '../../../common/constants/message_levels'; -import moment from 'moment'; +import type { JobSavedObjectService } from '../../saved_objects'; +import type { MlClient } from '../../lib/ml_client'; +import type { JobMessage } from '../../../common/types/audit_message'; +import { AuditMessage } from '../../../common/types/anomaly_detection_jobs'; const SIZE = 1000; -const LEVEL = { system_info: -1, info: 0, warning: 1, error: 2 }; +const LEVEL = { system_info: -1, info: 0, warning: 1, error: 2 } as const; + +type LevelName = keyof typeof LEVEL; +type LevelValue = typeof LEVEL[keyof typeof LEVEL]; // filter to match job_type: 'anomaly_detector' or no job_type field at all // if no job_type field exist, we can assume the message is for an anomaly detector job @@ -36,24 +46,40 @@ const anomalyDetectorTypeFilter = { }, }; -export function isClearable(index) { +export function isClearable(index?: string): boolean { if (typeof index === 'string') { const match = index.match(/\d{6}$/); - return match !== null && match.length && Number(match[match.length - 1]) >= 2; + return match !== null && !!match.length && Number(match[match.length - 1]) >= 2; } return false; } -export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { +export function jobAuditMessagesProvider( + { asInternalUser }: IScopedClusterClient, + mlClient: MlClient +) { // search for audit messages, // jobId is optional. without it, all jobs will be listed. // from is optional and should be a string formatted in ES time units. e.g. 12h, 1d, 7d - async function getJobAuditMessages(jobSavedObjectService, { jobId, from, start, end }) { + async function getJobAuditMessages( + jobSavedObjectService: JobSavedObjectService, + { + jobId, + from, + start, + end, + }: { + jobId?: string; + from?: string; + start?: string; + end?: string; + } + ) { let gte = null; if (jobId !== undefined && from === undefined) { const jobs = await mlClient.getJobs({ job_id: jobId }); - if (jobs.count > 0 && jobs.jobs !== undefined) { - gte = moment(jobs.jobs[0].create_time).valueOf(); + if (jobs.body.count > 0 && jobs.body.jobs !== undefined) { + gte = moment(jobs.body.jobs[0].create_time).valueOf(); } } else if (from !== undefined) { gte = `now-${from}`; @@ -120,7 +146,7 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }); } - const { body } = await asInternalUser.search({ + const { body } = await asInternalUser.search({ index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, size: SIZE, @@ -130,21 +156,21 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }, }); - let messages = []; - const notificationIndices = []; + let messages: JobMessage[] = []; + const notificationIndices: string[] = []; - if (body.hits.total.value > 0) { - let notificationIndex; + if ((body.hits.total as estypes.SearchTotalHits).value > 0) { + let notificationIndex: string; body.hits.hits.forEach((hit) => { if (notificationIndex !== hit._index && isClearable(hit._index)) { notificationIndices.push(hit._index); notificationIndex = hit._index; } - messages.push(hit._source); + messages.push(hit._source!); }); } - messages = await jobSavedObjectService.filterJobsForSpace( + messages = await jobSavedObjectService.filterJobsForSpace( 'anomaly-detector', messages, 'job_id' @@ -153,13 +179,14 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { } // search highest, most recent audit messages for all jobs for the last 24hrs. - async function getAuditMessagesSummary(jobIds) { + async function getAuditMessagesSummary(jobIds: string[]): Promise { // TODO This is the current default value of the cluster setting `search.max_buckets`. // This should possibly consider the real settings in a future update. const maxBuckets = 10000; - let levelsPerJobAggSize = maxBuckets; + const levelsPerJobAggSize = + Array.isArray(jobIds) && jobIds.length > 0 ? jobIds.length : maxBuckets; - const query = { + const query: QueryDslQueryContainer = { bool: { filter: [ { @@ -170,6 +197,15 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }, }, anomalyDetectorTypeFilter, + ...(Array.isArray(jobIds) && jobIds.length > 0 + ? [ + { + terms: { + job_id: jobIds, + }, + }, + ] + : []), ], must_not: { term: { @@ -179,17 +215,6 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }, }; - // If the jobIds arg is supplied, add a query filter - // to only include those jobIds in the aggregations. - if (Array.isArray(jobIds) && jobIds.length > 0) { - query.bool.filter.push({ - terms: { - job_id: jobIds, - }, - }); - levelsPerJobAggSize = jobIds.length; - } - const { body } = await asInternalUser.search({ index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, @@ -232,22 +257,39 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }, }); - let messagesPerJob = []; - const jobMessages = []; + interface LevelsPerJob { + key: string; + levels: estypes.AggregationsTermsAggregate<{ + key: LevelName; + latestMessage: estypes.AggregationsTermsAggregate<{ + key: string; + latestMessage: estypes.AggregationsValueAggregate; + }>; + }>; + } + + let messagesPerJob: LevelsPerJob[] = []; + + const jobMessages: AuditMessage[] = []; + + const bodyAgg = body.aggregations as { + levelsPerJob?: estypes.AggregationsTermsAggregate; + }; + if ( - body.hits.total.value > 0 && - body.aggregations && - body.aggregations.levelsPerJob && - body.aggregations.levelsPerJob.buckets && - body.aggregations.levelsPerJob.buckets.length + (body.hits.total as estypes.SearchTotalHits).value > 0 && + bodyAgg && + bodyAgg.levelsPerJob && + bodyAgg.levelsPerJob.buckets && + bodyAgg.levelsPerJob.buckets.length ) { - messagesPerJob = body.aggregations.levelsPerJob.buckets; + messagesPerJob = bodyAgg.levelsPerJob.buckets; } messagesPerJob.forEach((job) => { // ignore system messages (id==='') if (job.key !== '' && job.levels && job.levels.buckets && job.levels.buckets.length) { - let highestLevel = 0; + let highestLevel: LevelValue = 0; let highestLevelText = ''; let msgTime = 0; @@ -268,6 +310,7 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { // note the time in ms for the highest level // so we can filter them out later if they're earlier than the // job's create time. + if (msg.latestMessage && msg.latestMessage.value_as_string) { const time = moment(msg.latestMessage.value_as_string); msgTime = time.valueOf(); @@ -287,13 +330,17 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { } } }); + return jobMessages; } const clearedTime = new Date().getTime(); // Sets 'cleared' to true for messages in the last 24hrs and index new message for clear action - async function clearJobAuditMessages(jobId, notificationIndices) { + async function clearJobAuditMessages( + jobId: string, + notificationIndices: string[] + ): Promise<{ success: boolean; last_cleared: number }> { const newClearedMessage = { job_id: jobId, job_type: 'anomaly_detection', @@ -321,7 +368,7 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { }, }; - const promises = [ + const promises: Array> = [ asInternalUser.updateByQuery({ index: notificationIndices.join(','), ignore_unavailable: true, @@ -349,8 +396,8 @@ export function jobAuditMessagesProvider({ asInternalUser }, mlClient) { return { success: true, last_cleared: clearedTime }; } - function levelToText(level) { - return Object.keys(LEVEL)[Object.values(LEVEL).indexOf(level)]; + function levelToText(level: LevelValue): LevelName { + return (Object.keys(LEVEL) as LevelName[])[Object.values(LEVEL).indexOf(level)]; } return { From dd85150f73d92ce4ad6810f8a099ebce91a6d4c1 Mon Sep 17 00:00:00 2001 From: Sandra G Date: Fri, 13 Aug 2021 12:07:46 -0400 Subject: [PATCH 55/91] [Monitoring] Convert elasticsearch_settings dir to typescript (#108112) * convert elasticsearch_settings dir files to typescript * fix type * change tests to ts --- .../lib/elasticsearch_settings/cluster.js | 39 ---------------- .../{cluster.test.js => cluster.test.ts} | 25 +++++----- .../lib/elasticsearch_settings/cluster.ts | 46 +++++++++++++++++++ ...ind_reason.test.js => find_reason.test.ts} | 29 ++++++------ .../{find_reason.js => find_reason.ts} | 18 ++++---- .../{index.js => index.ts} | 0 .../{nodes.test.js => nodes.test.ts} | 11 +++-- .../{nodes.js => nodes.ts} | 5 +- ...ion_disabled.js => collection_disabled.ts} | 4 +- ...ction_enabled.js => collection_enabled.ts} | 4 +- ...ion_interval.js => collection_interval.ts} | 4 +- x-pack/plugins/monitoring/server/types.ts | 7 +++ 12 files changed, 110 insertions(+), 82 deletions(-) delete mode 100644 x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{cluster.test.js => cluster.test.ts} (85%) create mode 100644 x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.ts rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{find_reason.test.js => find_reason.test.ts} (91%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{find_reason.js => find_reason.ts} (92%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{nodes.test.js => nodes.test.ts} (92%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/{nodes.js => nodes.ts} (91%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/{collection_disabled.js => collection_disabled.ts} (87%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/{collection_enabled.js => collection_enabled.ts} (87%) rename x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/{collection_interval.js => collection_interval.ts} (87%) diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js deleted file mode 100644 index 1c20634c10220..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get } from 'lodash'; -import { findReason } from './find_reason'; - -export function handleResponse(response, isCloudEnabled) { - const sources = ['persistent', 'transient', 'defaults']; - for (const source of sources) { - const monitoringSettings = get(response[source], 'xpack.monitoring'); - if (monitoringSettings !== undefined) { - const check = findReason( - monitoringSettings, - { - context: `cluster ${source}`, - }, - isCloudEnabled - ); - - if (check.found) { - return check; - } - } - } - - return { found: false }; -} - -export async function checkClusterSettings(req) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); - const { cloud } = req.server.newPlatform.setup.plugins; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - const response = await callWithRequest(req, 'cluster.getSettings', { include_defaults: true }); - return handleResponse(response, isCloudEnabled); -} diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.ts similarity index 85% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.ts index c94b2bd4b0447..966a327e3084c 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.test.ts @@ -5,10 +5,15 @@ * 2.0. */ +import { ClusterGetSettingsResponse } from '@elastic/elasticsearch/api/types'; import { checkClusterSettings } from '.'; +import { LegacyRequest } from '../../types'; describe('Elasticsearch Cluster Settings', () => { - const makeResponse = (property, response = {}) => { + const makeResponse = ( + property: keyof ClusterGetSettingsResponse, + response: any = {} + ): ClusterGetSettingsResponse => { const result = { persistent: {}, transient: {}, @@ -18,8 +23,8 @@ describe('Elasticsearch Cluster Settings', () => { return result; }; - const getReq = (response) => { - return { + const getReq = (response: ClusterGetSettingsResponse) => { + return ({ server: { newPlatform: { setup: { @@ -40,20 +45,14 @@ describe('Elasticsearch Cluster Settings', () => { }, }, }, - }; + } as unknown) as LegacyRequest; }; - it('should return { found: false } given no response from ES', async () => { - const mockReq = getReq(makeResponse('ignore', {})); - const result = await checkClusterSettings(mockReq); - expect(result).toEqual({ found: false }); - }); - it('should find default collection interval reason', async () => { const setting = { xpack: { monitoring: { collection: { interval: -1 } } }, }; - const makeExpected = (source) => ({ + const makeExpected = (source: keyof ClusterGetSettingsResponse) => ({ found: true, reason: { context: `cluster ${source}`, @@ -82,7 +81,7 @@ describe('Elasticsearch Cluster Settings', () => { const setting = { xpack: { monitoring: { exporters: { myCoolExporter: {} } } }, }; - const makeExpected = (source) => ({ + const makeExpected = (source: keyof ClusterGetSettingsResponse) => ({ found: true, reason: { context: `cluster ${source}`, @@ -111,7 +110,7 @@ describe('Elasticsearch Cluster Settings', () => { const setting = { xpack: { monitoring: { enabled: 'false' } }, }; - const makeExpected = (source) => ({ + const makeExpected = (source: keyof ClusterGetSettingsResponse) => ({ found: true, reason: { context: `cluster ${source}`, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.ts new file mode 100644 index 0000000000000..4f46f65591d62 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/cluster.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash'; +import { ClusterGetSettingsResponse } from '@elastic/elasticsearch/api/types'; +import { findReason } from './find_reason'; +import { ClusterSettingsReasonResponse, LegacyRequest } from '../../types'; + +export function handleResponse( + response: ClusterGetSettingsResponse, + isCloudEnabled: boolean +): ClusterSettingsReasonResponse { + let source: keyof ClusterGetSettingsResponse; + for (source in response) { + if (Object.prototype.hasOwnProperty.call(response, source)) { + const monitoringSettings = get(response[source], 'xpack.monitoring'); + if (monitoringSettings !== undefined) { + const check = findReason( + monitoringSettings, + { + context: `cluster ${source}`, + }, + isCloudEnabled + ); + + if (check.found) { + return check; + } + } + } + } + + return { found: false }; +} + +export async function checkClusterSettings(req: LegacyRequest) { + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); + const { cloud } = req.server.newPlatform.setup.plugins; + const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); + const response = await callWithRequest(req, 'cluster.getSettings', { include_defaults: true }); + return handleResponse(response, isCloudEnabled); +} diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.ts similarity index 91% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.ts index 6031e01776734..a984a3a220306 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.test.ts @@ -19,7 +19,8 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { interval: -1, }, }, - context + context, + false ); expect(result).toEqual({ found: true, @@ -39,7 +40,8 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { enabled: false, }, }, - context + context, + false ); expect(result).toEqual({ found: true, @@ -61,7 +63,8 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { coolExporterToIgnore: {}, }, }, - context + context, + false ); expect(result).toEqual({ found: true, @@ -76,14 +79,14 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { describe('collection interval', () => { it('should not flag collection interval if value is > 0', async () => { - const result = await findReason({ collection: { interval: 1 } }, context); + const result = await findReason({ collection: { interval: 1 } }, context, false); expect(result).toEqual({ found: false }); }); it('should flag collection interval for any invalid value', async () => { let result; - result = await findReason({ collection: { interval: 0 } }, context); + result = await findReason({ collection: { interval: 0 } }, context, false); expect(result).toEqual({ found: true, reason: { @@ -93,7 +96,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, }); - result = await findReason({ collection: { interval: -10 } }, context); + result = await findReason({ collection: { interval: -10 } }, context, false); expect(result).toEqual({ found: true, reason: { @@ -103,7 +106,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, }); - result = await findReason({ collection: { interval: null } }, context); + result = await findReason({ collection: { interval: null } }, context, false); expect(result).toEqual({ found: true, reason: { @@ -116,16 +119,16 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }); it('should not flag enabled if value is true', async () => { - const result = await findReason({ enabled: true }, context); + const result = await findReason({ enabled: true }, context, false); expect(result).toEqual({ found: false }); }); it('should not flag exporters if value is undefined/null', async () => { let result; - result = await findReason({ exporters: undefined }, context); + result = await findReason({ exporters: undefined }, context, false); expect(result).toEqual({ found: false }); - result = await findReason({ exporters: null }, context); + result = await findReason({ exporters: null }, context, false); expect(result).toEqual({ found: false }); }); @@ -151,7 +154,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, }, }; - const result = await findReason(input, context); + const result = await findReason(input, context, false); expect(result).toEqual({ found: true, reason: { @@ -204,7 +207,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, }, }; - const result = await findReason(input, context); + const result = await findReason(input, context, false); expect(result).toEqual({ found: true, reason: { @@ -236,7 +239,7 @@ describe('Elasticsearch Settings Find Reason for No Data', () => { }, }, }; - const result = await findReason(input, context); + const result = await findReason(input, context, false); expect(result).toEqual({ found: false }); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.ts similarity index 92% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.ts index 6d4469d591c90..2e01856c1ed8f 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/find_reason.ts @@ -6,17 +6,22 @@ */ import { get } from 'lodash'; +import { ClusterSettingsReasonResponse } from '../../types'; /* * Return true if the settings property is enabled or is using its default state of enabled * Note: this assumes that a 0 corresponds to disabled */ -const isEnabledOrDefault = (property) => { +const isEnabledOrDefault = (property: string) => { return property === undefined || (Boolean(property) && property !== 'false'); }; -export function findReason(settingsSource, context, isCloudEnabled) { - const iterateReasons = () => { +export function findReason( + settingsSource: any, + context: { context: string }, + isCloudEnabled: boolean +) { + const iterateReasons = (): ClusterSettingsReasonResponse => { // PluginEnabled: check for `monitoring.enabled: false` const monitoringEnabled = get(settingsSource, 'enabled'); if (!isEnabledOrDefault(monitoringEnabled)) { @@ -92,9 +97,8 @@ export function findReason(settingsSource, context, isCloudEnabled) { return exporter.type !== 'local' && isEnabledOrDefault(exporter.enabled); }); if (allEnabledRemote.length > 0 && allEnabledLocal.length === 0) { - let ret = {}; if (isCloudEnabled) { - ret = { + return { found: true, reason: { property: 'xpack.monitoring.exporters.cloud_enabled', @@ -102,7 +106,7 @@ export function findReason(settingsSource, context, isCloudEnabled) { }, }; } else { - ret = { + return { found: true, reason: { property: 'xpack.monitoring.exporters', @@ -112,11 +116,9 @@ export function findReason(settingsSource, context, isCloudEnabled) { }, }; } - return ret; } } } - return { found: false }; }; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/index.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/index.ts diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.ts similarity index 92% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.ts index cc9707d2edf8b..7e1b93e50f5aa 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.test.ts @@ -6,10 +6,11 @@ */ import { checkNodesSettings } from '.'; +import { LegacyRequest } from '../../types'; describe('Elasticsearch Nodes Settings', () => { - const getReq = (response) => { - return { + const getReq = (response?: any) => { + return ({ server: { newPlatform: { setup: { @@ -23,12 +24,14 @@ describe('Elasticsearch Nodes Settings', () => { plugins: { elasticsearch: { getCluster() { - return { callWithRequest: () => Promise.resolve(response) }; + return { + callWithRequest: () => Promise.resolve(response), + }; }, }, }, }, - }; + } as unknown) as LegacyRequest; }; it('should return { found: false } given no response from ES', async () => { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.ts similarity index 91% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.ts index bc01b2578986d..3e428b47d6174 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/nodes.ts @@ -6,9 +6,10 @@ */ import { get } from 'lodash'; +import { LegacyRequest } from '../../types'; import { findReason } from './find_reason'; -export function handleResponse({ nodes = {} } = {}, isCloudEnabled) { +export function handleResponse({ nodes = {} } = {}, isCloudEnabled: boolean) { const nodeIds = Object.keys(nodes); for (const nodeId of nodeIds) { const nodeSettings = get(nodes, [nodeId, 'settings']); @@ -31,7 +32,7 @@ export function handleResponse({ nodes = {} } = {}, isCloudEnabled) { return { found: false }; } -export async function checkNodesSettings(req) { +export async function checkNodesSettings(req: LegacyRequest) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const { cloud } = req.server.newPlatform.setup.plugins; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_disabled.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_disabled.ts similarity index 87% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_disabled.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_disabled.ts index 049762f903578..44ad2a3634188 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_disabled.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_disabled.ts @@ -5,7 +5,9 @@ * 2.0. */ -export function setCollectionDisabled(req) { +import { LegacyRequest } from '../../../types'; + +export function setCollectionDisabled(req: LegacyRequest) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const params = { body: { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_enabled.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_enabled.ts similarity index 87% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_enabled.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_enabled.ts index 1208b95b085a6..a09dc6bb46ce9 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_enabled.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_enabled.ts @@ -5,7 +5,9 @@ * 2.0. */ -export function setCollectionEnabled(req) { +import { LegacyRequest } from '../../../types'; + +export function setCollectionEnabled(req: LegacyRequest) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const params = { body: { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_interval.js b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_interval.ts similarity index 87% rename from x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_interval.js rename to x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_interval.ts index c6677c7cfe245..873c1106f1aac 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_interval.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch_settings/set/collection_interval.ts @@ -5,7 +5,9 @@ * 2.0. */ -export function setCollectionInterval(req) { +import { LegacyRequest } from '../../../types'; + +export function setCollectionInterval(req: LegacyRequest) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const params = { body: { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 8120840aba03e..a0590c0f173fe 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -172,3 +172,10 @@ export interface Bucket { export interface Aggregation { buckets: Bucket[]; } +export interface ClusterSettingsReasonResponse { + found: boolean; + reason?: { + property?: string; + data?: string; + }; +} From e33daccca3b76aaa061206c7826991218f6fe941 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 13 Aug 2021 18:24:00 +0200 Subject: [PATCH 56/91] [Lens] Disable the global timepicker for index pattern without primary timefield and visualizations without timefield (#108052) --- .../lens/public/app_plugin/app.test.tsx | 48 ++++++++++- .../lens/public/app_plugin/lens_top_nav.tsx | 12 ++- .../indexpattern.test.ts | 79 +++++++++++++++++++ .../indexpattern_datasource/indexpattern.tsx | 10 +++ x-pack/plugins/lens/public/mocks.tsx | 5 +- .../time_range_middleware.test.ts | 4 +- x-pack/plugins/lens/public/types.ts | 4 + 7 files changed, 154 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index c26cce3317cf6..a10b0f436010f 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -269,6 +269,52 @@ describe('Lens App', () => { }); }); + describe('TopNavMenu#showDatePicker', () => { + it('shows date picker if any used index pattern isTimeBased', async () => { + const customServices = makeDefaultServices(sessionIdSubject); + customServices.data.indexPatterns.get = jest + .fn() + .mockImplementation((id) => + Promise.resolve({ id, isTimeBased: () => true } as IndexPattern) + ); + const { services } = await mountWith({ services: customServices }); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ showDatePicker: true }), + {} + ); + }); + it('shows date picker if active datasource isTimeBased', async () => { + const customServices = makeDefaultServices(sessionIdSubject); + customServices.data.indexPatterns.get = jest + .fn() + .mockImplementation((id) => + Promise.resolve({ id, isTimeBased: () => true } as IndexPattern) + ); + const customProps = makeDefaultProps(); + customProps.datasourceMap.testDatasource.isTimeBased = () => true; + const { services } = await mountWith({ props: customProps, services: customServices }); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ showDatePicker: true }), + {} + ); + }); + it('does not show date picker if index pattern nor active datasource is not time based', async () => { + const customServices = makeDefaultServices(sessionIdSubject); + customServices.data.indexPatterns.get = jest + .fn() + .mockImplementation((id) => + Promise.resolve({ id, isTimeBased: () => true } as IndexPattern) + ); + const customProps = makeDefaultProps(); + customProps.datasourceMap.testDatasource.isTimeBased = () => false; + const { services } = await mountWith({ props: customProps, services: customServices }); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ showDatePicker: false }), + {} + ); + }); + }); + describe('persistence', () => { it('passes query and indexPatterns to TopNavMenu', async () => { const { instance, lensStore, services } = await mountWith({ preloadedState: {} }); @@ -294,7 +340,7 @@ describe('Lens App', () => { expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', - indexPatterns: [{ id: 'mockip' }], + indexPatterns: [{ id: 'mockip', isTimeBased: expect.any(Function) }], }), {} ); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index f777d053b314b..c4c2a7523e589 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -166,6 +166,7 @@ export const LensTopNavMenu = ({ activeDatasourceId, datasourceStates, } = useLensSelector((state) => state.lens); + const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false); useEffect(() => { const activeDatasource = @@ -390,7 +391,16 @@ export const LensTopNavMenu = ({ dateRangeTo={to} indicateNoData={indicateNoData} showSearchBar={true} - showDatePicker={true} + showDatePicker={ + indexPatterns.some((ip) => ip.isTimeBased()) || + Boolean( + allLoaded && + activeDatasourceId && + datasourceMap[activeDatasourceId].isTimeBased( + datasourceStates[activeDatasourceId].state + ) + ) + } showQueryBar={true} showFilterBar={true} data-test-subj="lnsApp_topNav" diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index b3176dbcfe409..678644101d5ce 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -1544,4 +1544,83 @@ describe('IndexPattern Data Source', () => { }); }); }); + describe('#isTimeBased', () => { + it('should return true if date histogram exists in any layer', () => { + const state = enrichBaseState({ + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['metric'], + columns: { + metric: { + label: 'Count of records2', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + }, + second: { + indexPatternId: '1', + columnOrder: ['bucket1', 'bucket2', 'metric2'], + columns: { + metric2: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + bucket1: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: '1d', + }, + }, + bucket2: { + label: 'Terms', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'geo.src', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 10, + }, + }, + }, + }, + }, + }); + expect(indexPatternDatasource.isTimeBased(state)).toEqual(true); + }); + it('should return false if date histogram does not exist in any layer', () => { + const state = enrichBaseState({ + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['metric'], + columns: { + metric: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + }, + }, + }); + expect(indexPatternDatasource.isTimeBased(state)).toEqual(false); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 3a2d0df88a6cd..618cca418a8e7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -474,6 +474,16 @@ export function getIndexPatternDatasource({ const ids = Object.values(state.layers || {}).map(({ indexPatternId }) => indexPatternId); return ids.filter((id) => !state.indexPatterns[id]); }, + isTimeBased: (state) => { + const { layers } = state; + return ( + Boolean(layers) && + Object.values(layers).some((layer) => { + const buckets = layer.columnOrder.filter((colId) => layer.columns[colId].isBucketed); + return buckets.some((colId) => layer.columns[colId].operationType === 'date_histogram'); + }) + ); + }, }; return indexPatternDatasource; diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 03f03e2f3826c..d4c058c124639 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -147,6 +147,7 @@ export function createMockDatasource(id: string): DatasourceMock { publicAPIMock, getErrorMessages: jest.fn((_state) => undefined), checkIntegrity: jest.fn((_state) => []), + isTimeBased: jest.fn(), }; } @@ -309,9 +310,7 @@ export function mockDataPlugin(sessionIdSubject = new Subject()) { state$: new Observable(), }, indexPatterns: { - get: jest.fn((id) => { - return new Promise((resolve) => resolve({ id })); - }), + get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })), }, search: createMockSearchService(), nowProvider: { diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts index a3a53a6d380ed..8718f79f94782 100644 --- a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts +++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts @@ -117,9 +117,7 @@ function makeDefaultData(): jest.Mocked { state$: new Observable(), }, indexPatterns: { - get: jest.fn((id) => { - return new Promise((resolve) => resolve({ id })); - }), + get: jest.fn().mockImplementation((id) => Promise.resolve({ id, isTimeBased: () => true })), }, search: createMockSearchService(), nowProvider: { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index bf576cb65c688..0a04e4fea932d 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -265,6 +265,10 @@ export interface Datasource { * The frame calls this function to display warnings about visualization */ getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined; + /** + * Checks if the visualization created is time based, for example date histogram + */ + isTimeBased: (state: T) => boolean; } export interface DatasourceFixAction { From 57e395540d15f9971d4a2331bf733f65c69400f8 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Fri, 13 Aug 2021 10:51:38 -0600 Subject: [PATCH 57/91] [docs] Update Maps docs based on 7.14 UI. (#104762) --- docs/maps/images/app_gis_icon.png | Bin 0 -> 557 bytes docs/maps/maps-getting-started.asciidoc | 23 ++++++++++++----------- docs/maps/vector-layer.asciidoc | 2 ++ 3 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 docs/maps/images/app_gis_icon.png diff --git a/docs/maps/images/app_gis_icon.png b/docs/maps/images/app_gis_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5fbc0325841308881a8ee91bda050ab57e59d4bc GIT binary patch literal 557 zcmV+|0@D47P)(^xB>_oNB=7(L0R>P@R7L;)|MvFwt*xzAR#qAs8dq0W{{H?- zN=iULK(Vp0`}_MpKR-)LOa1-*tgNiV!^3iNa#~ti+1c3{85xR-id$P-P*702ySqF* zJo@_ju&}UjaB%nc_e@Mo4-XGbO-=6Z?jRr_#>U3;^Yird^dBD|;^N}n-QCjC(q?96 z(b3VQq@*n^E!^DPqobqc<>e_UDNasK%*@QczrPd|6p4w61_lPt&(Ful$C{d&CnqOK zNlCoCye1|lo12@!z`#;cQna+RuCA^(H#asmHjj^wI5;@`{QR@Cv&qTHxVX4NLPDXT zp~1nyXlQ6GEG$DqL%F%R%gf7+jg3`RRcdN#6B82{bG|SD006;BL_t(|oMT`B0!Aig z7FH%k1}I=-=iubx^ilo^y16`*_-B~>*BHB}{bFi%5MSW8<+S4U6Zz|e?+!C2UYpMk;D z45-;$RoH@oLB`U`+L*zH!I<6FPKJTO-ocT}$=Su##m&jx!@-k*&C6TIN7C2NSJKBn vAQ0^2AZEp2{b0qAAU2qfbVI{Jb-^qE&n6YU-1=1T00000NkvXXu0mjfgs=i; literal 0 HcmV?d00001 diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 97a5fc7ddaef4..548a574293403 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -34,8 +34,7 @@ refer to <>. . Open the main menu, and then click *Dashboard*. . Click **Create dashboard**. . Set the time range to *Last 7 days*. -. Click **Create panel**. -. Click **Maps**. +. Click the **Create new Maps** icon image:maps/images/app_gis_icon.png[] [float] [[maps-add-choropleth-layer]] @@ -62,14 +61,15 @@ and lighter shades will symbolize countries with less traffic. . Add a Tooltip field: -** Select **ISO 3166-1 alpha-2 code** and **name**. -** Click **Add**. +** **ISO 3166-1 alpha-2 code** is added by default. +** Click **+ Add** to open field select. +** Select **name** and click *Add*. -. In **Layer style**, set: +. In **Layer style**: -** **Fill color: As number** to the grey color ramp -** **Border color** to white -** **Label** to symbol label +** Set **Fill color: As number** to the grey color ramp. +** Set **Border color** to white. +** Under **Label**, change **By value** to **Fixed**. . Click **Save & close**. + @@ -135,9 +135,10 @@ grids with less bytes transferred. ** **Name** to `Total Requests and Bytes` ** **Visibility** to the range [0, 9] ** **Opacity** to 100% -. In **Metrics**, use: -** **Agregation** set to **Count**, and -** **Aggregation** set to **Sum** with **Field** set to **bytes** +. In **Metrics**: +** Set **Agregation** to **Count**. +** Click **Add metric**. +** Set **Aggregation** to **Sum** with **Field** set to **bytes**. . In **Layer style**, change **Symbol size**: ** Set the field select to *sum bytes*. ** Set the min size to 7 and the max size to 25 px. diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index 5017ecf91dffd..7191197c27dbe 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -14,6 +14,8 @@ To add a vector layer to your map, click *Add layer*, then select one of the fol *Clusters and grids*:: Geospatial data grouped in grids with metrics for each gridded cell. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. +*Create index*:: Draw shapes on the map and index in Elasticsearch. + *Documents*:: Points, lines, and polyons from Elasticsearch. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. + From e35be9d87c86a9340033193547a1b2c9a4be1bf7 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 13 Aug 2021 13:05:11 -0400 Subject: [PATCH 58/91] Fix flaky security/spaces tests (#108088) --- .../import/lib/check_origin_conflicts.test.ts | 20 +++++++++----- .../import/lib/check_origin_conflicts.ts | 20 ++++++++++---- .../common/suites/import.ts | 9 ++----- .../common/suites/copy_to_space.ts | 11 +++++--- .../common/suites/delete.ts | 27 +++++-------------- .../security_and_spaces/apis/delete.ts | 3 +-- 6 files changed, 44 insertions(+), 46 deletions(-) diff --git a/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts index 90693f29836d7..011e5500b8d9c 100644 --- a/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts @@ -23,14 +23,20 @@ type SavedObjectType = SavedObject<{ title?: string }>; type CheckOriginConflictsParams = Parameters[0]; /** - * Function to create a realistic-looking import object given a type, ID, and optional originId + * Function to create a realistic-looking import object given a type, ID, optional originId, and optional updated_at */ -const createObject = (type: string, id: string, originId?: string): SavedObjectType => ({ +const createObject = ( + type: string, + id: string, + originId?: string, + updatedAt?: string +): SavedObjectType => ({ type, id, attributes: { title: `Title for ${type}:${id}` }, references: (Symbol() as unknown) as SavedObjectReference[], ...(originId && { originId }), + ...(updatedAt && { updated_at: updatedAt }), }); const MULTI_NS_TYPE = 'multi'; @@ -389,21 +395,21 @@ describe('#checkOriginConflicts', () => { // try to import obj1 and obj2 const obj1 = createObject(MULTI_NS_TYPE, 'id-1'); const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo'); - const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id); - const objB = createObject(MULTI_NS_TYPE, 'id-B', obj1.id); + const objA = createObject(MULTI_NS_TYPE, 'id-A', obj1.id, '2017-09-21T18:59:16.270Z'); + const objB = createObject(MULTI_NS_TYPE, 'id-B', obj1.id, '2021-08-10T13:21:44.135Z'); const objC = createObject(MULTI_NS_TYPE, 'id-C', obj2.originId); const objD = createObject(MULTI_NS_TYPE, 'id-D', obj2.originId); const objects = [obj1, obj2]; const params = setupParams({ objects }); mockFindResult(objA, objB); // find for obj1: the result is an inexact match with two destinations - mockFindResult(objC, objD); // find for obj2: the result is an inexact match with two destinations + mockFindResult(objD, objC); // find for obj2: the result is an inexact match with two destinations const checkOriginConflictsResult = await checkOriginConflicts(params); const expectedResult = { importIdMap: new Map(), errors: [ - createAmbiguousConflictError(obj1, [objA, objB]), - createAmbiguousConflictError(obj2, [objC, objD]), + createAmbiguousConflictError(obj1, [objB, objA]), // Assert that these have been sorted by updatedAt in descending order + createAmbiguousConflictError(obj2, [objC, objD]), // Assert that these have been sorted by ID in ascending order (since their updatedAt values are the same) ], pendingOverwrites: new Set(), }; diff --git a/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts index 1952a04ab815c..d689f37f5ad26 100644 --- a/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts +++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts @@ -58,11 +58,21 @@ const createQuery = (type: string, id: string, rawIdPrefix: string) => const transformObjectsToAmbiguousConflictFields = ( objects: Array> ) => - objects.map(({ id, attributes, updated_at: updatedAt }) => ({ - id, - title: attributes?.title, - updatedAt, - })); + objects + .map(({ id, attributes, updated_at: updatedAt }) => ({ + id, + title: attributes?.title, + updatedAt, + })) + // Sort to ensure that integration tests are not flaky + .sort((a, b) => { + const aUpdatedAt = a.updatedAt ?? ''; + const bUpdatedAt = b.updatedAt ?? ''; + if (aUpdatedAt !== bUpdatedAt) { + return aUpdatedAt < bUpdatedAt ? 1 : -1; // descending + } + return a.id < b.id ? -1 : 1; // ascending + }); const getAmbiguousConflictSourceKey = ({ object }: InexactMatch) => `${object.type}:${object.originId || object.id}`; diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 34c53fc577094..3a8876a9dfae7 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -163,16 +163,11 @@ export function importTestSuiteFactory( type: 'conflict', ...(expectedNewId && { destinationId: expectedNewId }), }; - if (fail409Param === 'ambiguous_conflict_1a1b') { - // "ambiguous source" conflict - error = { - type: 'ambiguous_conflict', - destinations: [getConflictDest(`${CID}1`)], - }; - } else if (fail409Param === 'ambiguous_conflict_2c') { + if (fail409Param === 'ambiguous_conflict_2c') { // "ambiguous destination" conflict error = { type: 'ambiguous_conflict', + // response destinations should be sorted by updatedAt in descending order, then ID in ascending order destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)], }; } diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index d187228a83b17..73d1058bef2fc 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -571,15 +571,18 @@ export function copyToSpaceTestSuiteFactory( expectNewCopyResponse(response, ambiguousConflictId, title); } else { // It doesn't matter if overwrite is enabled or not, the object will not be copied because there are two matches in the destination space - const updatedAt = '2017-09-21T18:59:16.270Z'; const destinations = [ - // response should be sorted by updatedAt in descending order + // response destinations should be sorted by updatedAt in descending order, then ID in ascending order + { + id: 'conflict_2_all', + title: 'A shared saved-object in all spaces', + updatedAt: '2017-09-21T18:59:16.270Z', + }, { id: 'conflict_2_space_2', title: 'A shared saved-object in one space', - updatedAt, + updatedAt: '2017-09-21T18:59:16.270Z', }, - { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt }, ]; expect(success).to.eql(false); expect(successCount).to.eql(0); diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index ccd08fb2d93e9..2e261d3c93bae 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -49,7 +49,10 @@ export function deleteTestSuiteFactory( size: 0, query: { terms: { - type: ['visualization', 'dashboard', 'space', 'config', 'index-pattern'], + type: ['visualization', 'dashboard', 'space', 'index-pattern'], + // TODO: add assertions for config objects -- these assertions were removed because of flaky behavior in #92358, but we should + // consider adding them again at some point, especially if we convert config objects to `namespaceType: 'multiple-isolated'` in + // the future. }, }, aggs: { @@ -80,7 +83,7 @@ export function deleteTestSuiteFactory( const expectedBuckets = [ { key: 'default', - doc_count: 9, + doc_count: 8, countByType: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, @@ -97,10 +100,6 @@ export function deleteTestSuiteFactory( key: 'space', doc_count: 2, }, - { - key: 'config', - doc_count: 1, - }, { key: 'index-pattern', doc_count: 1, @@ -109,7 +108,7 @@ export function deleteTestSuiteFactory( }, }, { - doc_count: 7, + doc_count: 6, key: 'space_1', countByType: { doc_count_error_upper_bound: 0, @@ -123,10 +122,6 @@ export function deleteTestSuiteFactory( key: 'dashboard', doc_count: 2, }, - { - key: 'config', - doc_count: 1, - }, { key: 'index-pattern', doc_count: 1, @@ -190,16 +185,6 @@ export function deleteTestSuiteFactory( await esArchiver.load( 'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces' ); - - // since we want to verify that we only delete the right things - // and can't include a config document with the correct id in the - // archive we read the settings to trigger an automatic upgrade - // in each space - await supertest.get('/api/kibana/settings').auth(user.username, user.password).expect(200); - await supertest - .get('/s/space_1/api/kibana/settings') - .auth(user.username, user.password) - .expect(200); }); afterEach(() => esArchiver.unload( diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts index b22e24d33c246..2ba0e4d77bfbe 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/delete.ts @@ -24,8 +24,7 @@ export default function deleteSpaceTestSuite({ getService }: FtrProviderContext) expectReservedSpaceResult, } = deleteTestSuiteFactory(es, esArchiver, supertestWithoutAuth); - // FLAKY: https://github.com/elastic/kibana/issues/92358 - describe.skip('delete', () => { + describe('delete', () => { [ { spaceId: SPACES.DEFAULT.spaceId, From f3e094c836cd5d8ea22e8c85a9c06db9e8b4cca1 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Fri, 13 Aug 2021 13:08:46 -0400 Subject: [PATCH 59/91] [App Search] Added a CrawlRulesTable to the Crawler view (#108458) --- .../components/crawl_rules_table.test.tsx | 341 ++++++++++++++++++ .../crawler/components/crawl_rules_table.tsx | 203 +++++++++++ .../crawler/crawler_single_domain.tsx | 10 + .../crawler_single_domain_logic.test.ts | 36 +- .../crawler/crawler_single_domain_logic.ts | 6 +- .../app_search/components/crawler/types.ts | 52 +++ .../reorderable_table/reorderable_table.tsx | 31 +- .../server/routes/app_search/crawler.test.ts | 43 +++ .../server/routes/app_search/crawler.ts | 23 ++ .../app_search/crawler_crawl_rules.test.ts | 139 +++++++ .../routes/app_search/crawler_crawl_rules.ts | 84 +++++ .../server/routes/app_search/index.ts | 2 + 12 files changed, 953 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.tsx create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.test.tsx new file mode 100644 index 0000000000000..90998a31fa273 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.test.tsx @@ -0,0 +1,341 @@ +/* + * 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 { mockFlashMessageHelpers, setMockActions } from '../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiFieldText, EuiSelect } from '@elastic/eui'; + +import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; + +import { CrawlerPolicies, CrawlerRules } from '../types'; + +import { CrawlRulesTable } from './crawl_rules_table'; + +describe('CrawlRulesTable', () => { + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; + const engineName = 'my-engine'; + const crawlRules = [ + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + { id: '2', pattern: '*', policy: CrawlerPolicies.deny, rule: CrawlerRules.endsWith }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(GenericEndpointInlineEditableTable).exists()).toBe(true); + }); + + describe('columns', () => { + const crawlRule = { + id: '1', + pattern: '*', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.beginsWith, + }; + let wrapper: ShallowWrapper; + + beforeEach(() => { + wrapper = shallow( + + ); + }); + + const renderColumn = (index: number) => { + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + return shallow(
{columns[index].render(crawlRule)}
); + }; + + const onChange = jest.fn(); + const renderColumnInEditingMode = (index: number) => { + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + return shallow( +
+ {columns[index].editingRender(crawlRule, onChange, { + isInvalid: false, + isLoading: false, + })} +
+ ); + }; + + describe('policy column', () => { + it('shows the policy of a crawl rule', () => { + expect(renderColumn(0).html()).toContain('Allow'); + }); + + it('can show the policy of a crawl rule as editable', () => { + const column = renderColumnInEditingMode(0); + + const selectField = column.find(EuiSelect); + expect(selectField.props()).toEqual( + expect.objectContaining({ + value: 'allow', + disabled: false, + isInvalid: false, + options: [ + { text: 'Allow', value: 'allow' }, + { text: 'Disallow', value: 'deny' }, + ], + }) + ); + + selectField.simulate('change', { target: { value: 'deny' } }); + expect(onChange).toHaveBeenCalledWith('deny'); + }); + }); + + describe('rule column', () => { + it('shows the rule of a crawl rule', () => { + expect(renderColumn(1).html()).toContain('Begins with'); + }); + + it('can show the rule of a crawl rule as editable', () => { + const column = renderColumnInEditingMode(1); + + const selectField = column.find(EuiSelect); + expect(selectField.props()).toEqual( + expect.objectContaining({ + value: 'begins', + disabled: false, + isInvalid: false, + options: [ + { text: 'Begins with', value: 'begins' }, + { text: 'Ends with', value: 'ends' }, + { text: 'Contains', value: 'contains' }, + { text: 'Regex', value: 'regex' }, + ], + }) + ); + + selectField.simulate('change', { target: { value: 'ends' } }); + expect(onChange).toHaveBeenCalledWith('ends'); + }); + }); + + describe('pattern column', () => { + it('shows the pattern of a crawl rule', () => { + expect(renderColumn(2).html()).toContain('*'); + }); + + it('can show the pattern of a crawl rule as editable', () => { + const column = renderColumnInEditingMode(2); + + const field = column.find(EuiFieldText); + expect(field.props()).toEqual( + expect.objectContaining({ + value: '*', + disabled: false, + isInvalid: false, + }) + ); + + field.simulate('change', { target: { value: 'foo' } }); + expect(onChange).toHaveBeenCalledWith('foo'); + }); + }); + }); + + describe('routes', () => { + it('can calculate an update and delete route correctly', () => { + const wrapper = shallow( + + ); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const crawlRule = { + id: '1', + pattern: '*', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.beginsWith, + }; + expect(table.prop('deleteRoute')(crawlRule)).toEqual( + '/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/crawl_rules/1' + ); + expect(table.prop('updateRoute')(crawlRule)).toEqual( + '/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/crawl_rules/1' + ); + }); + }); + + it('shows a custom description if one is provided', () => { + const wrapper = shallow( + + ); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + expect(table.prop('description')).toEqual('I am a description'); + }); + + it('shows a default crawl rule as uneditable if one is provided', () => { + const wrapper = shallow( + + ); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + expect(table.prop('uneditableItems')).toEqual([crawlRules[0]]); + }); + + describe('when a crawl rule is added', () => { + it('should update the crawl rules for the current domain, and clear flash messages', () => { + const updateCrawlRules = jest.fn(); + setMockActions({ + updateCrawlRules, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const crawlRulesThatWasAdded = { + id: '2', + pattern: '*', + policy: CrawlerPolicies.deny, + rule: CrawlerRules.endsWith, + }; + const updatedCrawlRules = [ + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + { id: '2', pattern: '*', policy: CrawlerPolicies.deny, rule: CrawlerRules.endsWith }, + ]; + table.prop('onAdd')(crawlRulesThatWasAdded, updatedCrawlRules); + expect(updateCrawlRules).toHaveBeenCalledWith(updatedCrawlRules); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when a crawl rule is updated', () => { + it('should update the crawl rules for the current domain, and clear flash messages', () => { + const updateCrawlRules = jest.fn(); + setMockActions({ + updateCrawlRules, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const crawlRulesThatWasUpdated = { + id: '2', + pattern: '*', + policy: CrawlerPolicies.deny, + rule: CrawlerRules.endsWith, + }; + const updatedCrawlRules = [ + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + { + id: '2', + pattern: 'newPattern', + policy: CrawlerPolicies.deny, + rule: CrawlerRules.endsWith, + }, + ]; + table.prop('onUpdate')(crawlRulesThatWasUpdated, updatedCrawlRules); + expect(updateCrawlRules).toHaveBeenCalledWith(updatedCrawlRules); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('when a crawl rule is deleted', () => { + it('should update the crawl rules for the current domain, clear flash messages, and show a success', () => { + const updateCrawlRules = jest.fn(); + setMockActions({ + updateCrawlRules, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const crawlRulesThatWasDeleted = { + id: '2', + pattern: '*', + policy: CrawlerPolicies.deny, + rule: CrawlerRules.endsWith, + }; + const updatedCrawlRules = [ + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + ]; + table.prop('onDelete')(crawlRulesThatWasDeleted, updatedCrawlRules); + expect(updateCrawlRules).toHaveBeenCalledWith(updatedCrawlRules); + expect(clearFlashMessages).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); + }); + }); + + describe('when a crawl rule is reordered', () => { + it('should update the crawl rules for the current domain and clear flash messages', () => { + const updateCrawlRules = jest.fn(); + setMockActions({ + updateCrawlRules, + }); + const wrapper = shallow( + + ); + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const updatedCrawlRules = [ + { id: '2', pattern: '*', policy: CrawlerPolicies.deny, rule: CrawlerRules.endsWith }, + { id: '1', pattern: '*', policy: CrawlerPolicies.allow, rule: CrawlerRules.beginsWith }, + ]; + table.prop('onReorder')!(updatedCrawlRules); + expect(updateCrawlRules).toHaveBeenCalledWith(updatedCrawlRules); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.tsx new file mode 100644 index 0000000000000..9af8cb66fdc4f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_rules_table.tsx @@ -0,0 +1,203 @@ +/* + * 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 { useActions } from 'kea'; + +import { EuiCode, EuiFieldText, EuiLink, EuiSelect, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { clearFlashMessages, flashSuccessToast } from '../../../../shared/flash_messages'; +import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; +import { InlineEditableTableColumn } from '../../../../shared/tables/inline_editable_table/types'; +import { ItemWithAnID } from '../../../../shared/tables/types'; +import { DOCS_PREFIX } from '../../../routes'; +import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic'; +import { + CrawlerPolicies, + CrawlerRules, + CrawlRule, + getReadableCrawlerPolicy, + getReadableCrawlerRule, +} from '../types'; + +interface CrawlRulesTableProps { + description?: React.ReactNode; + domainId: string; + engineName: string; + crawlRules: CrawlRule[]; + defaultCrawlRule?: CrawlRule; +} + +const DEFAULT_DESCRIPTION = ( +

+ + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesTable.descriptionLinkText', + { defaultMessage: 'Learn more about crawl rules' } + )} + + ), + }} + /> +

+); + +export const CrawlRulesTable: React.FC = ({ + description = DEFAULT_DESCRIPTION, + domainId, + engineName, + crawlRules, + defaultCrawlRule, +}) => { + const { updateCrawlRules } = useActions(CrawlerSingleDomainLogic); + + const columns: Array> = [ + { + editingRender: (crawlRule, onChange, { isInvalid, isLoading }) => ( + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + options={[CrawlerPolicies.allow, CrawlerPolicies.deny].map( + (policyOption: CrawlerPolicies) => ({ + text: getReadableCrawlerPolicy(policyOption), + value: policyOption, + }) + )} + /> + ), + render: (crawlRule) => ( + {getReadableCrawlerPolicy((crawlRule as CrawlRule).policy)} + ), + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesTable.policyTableHead', + { + defaultMessage: 'Policy', + } + ), + field: 'policy', + }, + { + editingRender: (crawlRule, onChange, { isInvalid, isLoading }) => ( + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + options={[ + CrawlerRules.beginsWith, + CrawlerRules.endsWith, + CrawlerRules.contains, + CrawlerRules.regex, + ].map((ruleOption: CrawlerRules) => ({ + text: getReadableCrawlerRule(ruleOption), + value: ruleOption, + }))} + /> + ), + render: (crawlRule) => ( + {getReadableCrawlerRule((crawlRule as CrawlRule).rule)} + ), + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesTable.ruleTableHead', + { + defaultMessage: 'Rule', + } + ), + field: 'rule', + }, + { + editingRender: (crawlRule, onChange, { isInvalid, isLoading }) => ( + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + /> + ), + render: (crawlRule) => {(crawlRule as CrawlRule).pattern}, + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesTable.pathPatternTableHead', + { + defaultMessage: 'Path pattern', + } + ), + field: 'pattern', + }, + ]; + + const crawlRulesRoute = `/api/app_search/engines/${engineName}/crawler/domains/${domainId}/crawl_rules`; + const domainRoute = `/api/app_search/engines/${engineName}/crawler/domains/${domainId}`; + const getCrawlRuleRoute = (crawlRule: CrawlRule) => + `/api/app_search/engines/${engineName}/crawler/domains/${domainId}/crawl_rules/${crawlRule.id}`; + + return ( + { + updateCrawlRules(newCrawlRules as CrawlRule[]); + clearFlashMessages(); + }} + onDelete={(_, newCrawlRules) => { + updateCrawlRules(newCrawlRules as CrawlRule[]); + clearFlashMessages(); + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesTable.deleteSuccessToastMessage', + { + defaultMessage: 'The crawl rule has been deleted.', + } + ) + ); + }} + onUpdate={(_, newCrawlRules) => { + updateCrawlRules(newCrawlRules as CrawlRule[]); + clearFlashMessages(); + }} + onReorder={(newCrawlRules) => { + updateCrawlRules(newCrawlRules as CrawlRule[]); + clearFlashMessages(); + }} + title={i18n.translate('xpack.enterpriseSearch.appSearch.crawler.crawlRulesTable.title', { + defaultMessage: 'Crawl rules', + })} + uneditableItems={defaultCrawlRule ? [defaultCrawlRule] : undefined} + canRemoveLastItem + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index 464ecbe157c4f..b93fb8592cff8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n'; import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; +import { CrawlRulesTable } from './components/crawl_rules_table'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeleteDomainPanel } from './components/delete_domain_panel'; @@ -64,6 +65,15 @@ export const CrawlerSingleDomain: React.FC = () => { + + + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index 60f3aca7794eb..492bd363a5f2d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -16,7 +16,7 @@ import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { CrawlerSingleDomainLogic, CrawlerSingleDomainValues } from './crawler_single_domain_logic'; -import { CrawlerDomain } from './types'; +import { CrawlerDomain, CrawlerPolicies, CrawlerRules } from './types'; const DEFAULT_VALUES: CrawlerSingleDomainValues = { dataLoading: true, @@ -111,6 +111,40 @@ describe('CrawlerSingleDomainLogic', () => { }); }); }); + + describe('updateCrawlRules', () => { + beforeEach(() => { + mount({ + domain: { + id: '507f1f77bcf86cd799439011', + crawlRules: [], + }, + }); + + CrawlerSingleDomainLogic.actions.updateCrawlRules([ + { + id: '1234', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.beginsWith, + pattern: 'foo', + }, + ]); + }); + + it('should update the crawl rules on the domain', () => { + expect(CrawlerSingleDomainLogic.values.domain).toEqual({ + id: '507f1f77bcf86cd799439011', + crawlRules: [ + { + id: '1234', + policy: CrawlerPolicies.allow, + rule: CrawlerRules.beginsWith, + pattern: 'foo', + }, + ], + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts index 24830e9d727ca..78912f736926d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts @@ -14,7 +14,7 @@ import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_CRAWLER_PATH } from '../../routes'; import { EngineLogic, generateEnginePath } from '../engine'; -import { CrawlerDomain, EntryPoint, Sitemap } from './types'; +import { CrawlerDomain, EntryPoint, Sitemap, CrawlRule } from './types'; import { crawlerDomainServerToClient, getDeleteDomainSuccessMessage } from './utils'; export interface CrawlerSingleDomainValues { @@ -26,6 +26,7 @@ interface CrawlerSingleDomainActions { deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain }; fetchDomainData(domainId: string): { domainId: string }; onReceiveDomainData(domain: CrawlerDomain): { domain: CrawlerDomain }; + updateCrawlRules(crawlRules: CrawlRule[]): { crawlRules: CrawlRule[] }; updateEntryPoints(entryPoints: EntryPoint[]): { entryPoints: EntryPoint[] }; updateSitemaps(entryPoints: Sitemap[]): { sitemaps: Sitemap[] }; } @@ -38,6 +39,7 @@ export const CrawlerSingleDomainLogic = kea< deleteDomain: (domain) => ({ domain }), fetchDomainData: (domainId) => ({ domainId }), onReceiveDomainData: (domain) => ({ domain }), + updateCrawlRules: (crawlRules) => ({ crawlRules }), updateEntryPoints: (entryPoints) => ({ entryPoints }), updateSitemaps: (sitemaps) => ({ sitemaps }), }, @@ -52,6 +54,8 @@ export const CrawlerSingleDomainLogic = kea< null, { onReceiveDomainData: (_, { domain }) => domain, + updateCrawlRules: (currentDomain, { crawlRules }) => + ({ ...currentDomain, crawlRules } as CrawlerDomain), updateEntryPoints: (currentDomain, { entryPoints }) => ({ ...currentDomain, entryPoints } as CrawlerDomain), updateSitemaps: (currentDomain, { sitemaps }) => diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 0902499feb4ed..1b46e21dbcb72 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -12,6 +12,25 @@ export enum CrawlerPolicies { deny = 'deny', } +export const getReadableCrawlerPolicy = (policy: CrawlerPolicies) => { + switch (policy) { + case CrawlerPolicies.allow: + return i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesPolicies.allowLabel', + { + defaultMessage: 'Allow', + } + ); + case CrawlerPolicies.deny: + return i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesPolicies.disallowLabel', + { + defaultMessage: 'Disallow', + } + ); + } +}; + export enum CrawlerRules { beginsWith = 'begins', endsWith = 'ends', @@ -19,6 +38,39 @@ export enum CrawlerRules { regex = 'regex', } +export const getReadableCrawlerRule = (rule: CrawlerRules) => { + switch (rule) { + case CrawlerRules.beginsWith: + return i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesCrawlerRules.beginsWithLabel', + { + defaultMessage: 'Begins with', + } + ); + case CrawlerRules.endsWith: + return i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesCrawlerRules.endsWithLabel', + { + defaultMessage: 'Ends with', + } + ); + case CrawlerRules.contains: + return i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesCrawlerRules.containsLabel', + { + defaultMessage: 'Contains', + } + ); + case CrawlerRules.regex: + return i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlRulesCrawlerRules.regexLabel', + { + defaultMessage: 'Regex', + } + ); + } +}; + export interface CrawlRule { id: string; policy: CrawlerPolicies; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx index f43b940fbf3e5..4cb12321bdfcf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/reorderable_table/reorderable_table.tsx @@ -47,7 +47,7 @@ export const ReorderableTable = ({
: undefined} /> - {items.length === 0 && ( + {items.length === 0 && unreorderableItems.length === 0 && ( {noItemsMessage} @@ -71,20 +71,6 @@ export const ReorderableTable = ({ )} onReorder={onReorder} /> - {unreorderableItems.length > 0 && ( - ( - } - /> - )} - /> - )} )} @@ -101,6 +87,21 @@ export const ReorderableTable = ({ )} /> )} + + {unreorderableItems.length > 0 && ( + ( + } + /> + )} + /> + )}
); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index b0e286077f838..38cae6d5d7f7c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -244,6 +244,49 @@ describe('crawler routes', () => { }); }); + describe('PUT /api/app_search/engines/{name}/crawler/domains/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/app_search/engines/{name}/crawler/domains/{id}', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/domains/:id', + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { name: 'some-engine', id: '1234' }, + body: { + crawl_rules: [ + { + order: 1, + id: '5678', + }, + ], + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + describe('GET /api/app_search/engines/{name}/crawler/domains/{id}', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index 1ba7885664ff3..79664d45dbbd8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -127,6 +127,29 @@ export function registerCrawlerRoutes({ }) ); + router.put( + { + path: '/api/app_search/engines/{name}/crawler/domains/{id}', + validate: { + params: schema.object({ + name: schema.string(), + id: schema.string(), + }), + body: schema.object({ + crawl_rules: schema.arrayOf( + schema.object({ + order: schema.number(), + id: schema.string(), + }) + ), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/domains/:id', + }) + ); + router.post( { path: '/api/app_search/crawler/validate_url', diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts new file mode 100644 index 0000000000000..ec131c7cd1981 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; + +import { registerCrawlerCrawlRulesRoutes } from './crawler_crawl_rules'; + +describe('crawler crawl rules routes', () => { + describe('POST /api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules', + }); + + registerCrawlerCrawlRulesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234' }, + body: { + pattern: '*', + policy: 'allow', + rule: 'begins', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + describe('PUT /api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', + }); + + registerCrawlerCrawlRulesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234', crawlRuleId: '5678' }, + body: { + order: 1, + pattern: '*', + policy: 'allow', + rule: 'begins', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + describe('DELETE /api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', + }); + + registerCrawlerCrawlRulesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234', crawlRuleId: '5678' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts new file mode 100644 index 0000000000000..9367ba4492558 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerCrawlerCrawlRulesRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + }), + body: schema.object({ + pattern: schema.string(), + policy: schema.string(), + rule: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules', + params: { + respond_with: 'index', + }, + }) + ); + + router.put( + { + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + crawlRuleId: schema.string(), + }), + body: schema.object({ + order: schema.number(), + pattern: schema.string(), + policy: schema.string(), + rule: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + params: { + respond_with: 'index', + }, + }) + ); + + router.delete( + { + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/crawl_rules/{crawlRuleId}', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + crawlRuleId: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + params: { + respond_with: 'index', + }, + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 3f794421348d7..f6979bce0e780 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -10,6 +10,7 @@ import { RouteDependencies } from '../../plugin'; import { registerAnalyticsRoutes } from './analytics'; import { registerApiLogsRoutes } from './api_logs'; import { registerCrawlerRoutes } from './crawler'; +import { registerCrawlerCrawlRulesRoutes } from './crawler_crawl_rules'; import { registerCrawlerEntryPointRoutes } from './crawler_entry_points'; import { registerCrawlerSitemapRoutes } from './crawler_sitemaps'; import { registerCredentialsRoutes } from './credentials'; @@ -47,5 +48,6 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerOnboardingRoutes(dependencies); registerCrawlerRoutes(dependencies); registerCrawlerEntryPointRoutes(dependencies); + registerCrawlerCrawlRulesRoutes(dependencies); registerCrawlerSitemapRoutes(dependencies); }; From 44014c78b6673cfb8dfc9ecbcb7cba085eb985f2 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Fri, 13 Aug 2021 12:08:50 -0500 Subject: [PATCH 60/91] [canvas] Create Custom Elements Service (#107356) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../saved_elements_modal.stories.tsx | 31 ++-- .../saved_elements_modal.component.tsx | 59 +++----- .../saved_elements_modal.ts | 138 ------------------ .../saved_elements_modal.tsx | 110 ++++++++++++++ .../public/lib/custom_element_service.ts | 43 ------ .../public/lib/element_handler_creators.ts | 2 +- .../canvas/public/services/custom_element.ts | 21 +++ .../plugins/canvas/public/services/index.ts | 12 +- .../public/services/kibana/custom_element.ts | 41 ++++++ .../canvas/public/services/kibana/index.ts | 7 +- .../public/services/stubs/custom_element.ts | 21 +++ .../canvas/public/services/stubs/index.ts | 3 + 12 files changed, 246 insertions(+), 242 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts create mode 100644 x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx delete mode 100644 x-pack/plugins/canvas/public/lib/custom_element_service.ts create mode 100644 x-pack/plugins/canvas/public/services/custom_element.ts create mode 100644 x-pack/plugins/canvas/public/services/kibana/custom_element.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/custom_element.ts diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx index 086a4be140211..50e9ee8ac5a89 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/saved_elements_modal.stories.tsx @@ -17,13 +17,11 @@ storiesOf('components/SavedElementsModal', module) .add('no custom elements', () => ( )) .add( @@ -31,13 +29,11 @@ storiesOf('components/SavedElementsModal', module) (_, props) => ( ), { decorators: [waitFor(getTestCustomElements())] } @@ -47,13 +43,12 @@ storiesOf('components/SavedElementsModal', module) (_, props) => ( ), { decorators: [waitFor(getTestCustomElements())] } diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx index ee14e89dc4b7d..1e508d2d825a3 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx @@ -13,7 +13,6 @@ import React, { useEffect, useRef, } from 'react'; -import PropTypes from 'prop-types'; import { EuiModal, EuiModalBody, @@ -81,66 +80,62 @@ const strings = { export interface Props { /** - * Adds the custom element to the workpad + * Element add handler */ - addCustomElement: (customElement: CustomElement) => void; - /** - * Queries ES for custom element saved objects - */ - findCustomElements: () => void; + onAddCustomElement: (customElement: CustomElement) => void; /** * Handler invoked when the modal closes */ onClose: () => void; /** - * Deletes the custom element + * Element delete handler */ - removeCustomElement: (id: string) => void; + onRemoveCustomElement: (id: string) => void; /** - * Saved edits to the custom element + * Element update handler */ - updateCustomElement: (id: string, name: string, description: string, image: string) => void; + onUpdateCustomElement: (id: string, name: string, description: string, image: string) => void; /** * Array of custom elements to display */ customElements: CustomElement[]; /** - * Text used to filter custom elements list + * Element search handler */ - search: string; + onSearch: (search: string) => void; /** - * Setter for search text + * Initial search term */ - setSearch: (search: string) => void; + initialSearch?: string; } export const SavedElementsModal: FunctionComponent = ({ - search, - setSearch, customElements, - addCustomElement, - findCustomElements, + onAddCustomElement, onClose, - removeCustomElement, - updateCustomElement, + onRemoveCustomElement, + onUpdateCustomElement, + onSearch, + initialSearch = '', }) => { const hasLoadedElements = useRef(false); const [elementToDelete, setElementToDelete] = useState(null); const [elementToEdit, setElementToEdit] = useState(null); + const [search, setSearch] = useState(initialSearch); useEffect(() => { if (!hasLoadedElements.current) { hasLoadedElements.current = true; - findCustomElements(); + onSearch(''); } - }, [findCustomElements, hasLoadedElements]); + }, [onSearch, hasLoadedElements]); const showEditModal = (element: CustomElement) => setElementToEdit(element); const hideEditModal = () => setElementToEdit(null); const handleEdit = async (name: string, description: string, image: string) => { if (elementToEdit) { - updateCustomElement(elementToEdit.id, name, description, image); + onUpdateCustomElement(elementToEdit.id, name, description, image); } hideEditModal(); }; @@ -150,7 +145,7 @@ export const SavedElementsModal: FunctionComponent = ({ const handleDelete = async () => { if (elementToDelete) { - removeCustomElement(elementToDelete.id); + onRemoveCustomElement(elementToDelete.id); } hideDeleteModal(); }; @@ -193,7 +188,7 @@ export const SavedElementsModal: FunctionComponent = ({ const sortElements = (elements: CustomElement[]): CustomElement[] => sortBy(elements, 'displayName'); - const onSearch = (e: ChangeEvent) => setSearch(e.target.value); + const onFieldSearch = (e: ChangeEvent) => setSearch(e.target.value); let customElementContent = ( = ({ @@ -235,7 +230,7 @@ export const SavedElementsModal: FunctionComponent = ({ fullWidth value={search} placeholder={strings.getFindElementPlaceholder()} - onChange={onSearch} + onChange={onFieldSearch} /> {customElementContent} @@ -252,11 +247,3 @@ export const SavedElementsModal: FunctionComponent = ({ ); }; - -SavedElementsModal.propTypes = { - addCustomElement: PropTypes.func.isRequired, - findCustomElements: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - removeCustomElement: PropTypes.func.isRequired, - updateCustomElement: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts deleted file mode 100644 index 524c1a48b6cee..0000000000000 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { compose, withState } from 'recompose'; -import { camelCase } from 'lodash'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import * as customElementService from '../../lib/custom_element_service'; -import { withServices, WithServicesProps, pluginServices } from '../../services'; -// @ts-expect-error untyped local -import { selectToplevelNodes } from '../../state/actions/transient'; -// @ts-expect-error untyped local -import { insertNodes } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { - SavedElementsModal as Component, - Props as ComponentProps, -} from './saved_elements_modal.component'; -import { State, PositionedElement, CustomElement } from '../../../types'; - -const customElementAdded = 'elements-custom-added'; - -interface OwnProps { - onClose: () => void; -} - -interface OwnPropsWithState extends OwnProps { - customElements: CustomElement[]; - setCustomElements: (customElements: CustomElement[]) => void; - search: string; - setSearch: (search: string) => void; -} - -interface DispatchProps { - selectToplevelNodes: (nodes: PositionedElement[]) => void; - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; -} - -interface StateProps { - pageId: string; -} - -const mapStateToProps = (state: State): StateProps => ({ - pageId: getSelectedPage(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - selectToplevelNodes: (nodes: PositionedElement[]) => - dispatch( - selectToplevelNodes( - nodes - .filter((e: PositionedElement): boolean => !e.position.parent) - .map((e: PositionedElement): string => e.id) - ) - ), - insertNodes: (selectedNodes: PositionedElement[], pageId: string) => - dispatch(insertNodes(selectedNodes, pageId)), -}); - -const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ownProps: OwnPropsWithState & WithServicesProps -): ComponentProps => { - const notifyService = pluginServices.getServices().notify; - const { pageId } = stateProps; - const { onClose, search, setCustomElements } = ownProps; - - const findCustomElements = async () => { - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - }; - - return { - ...ownProps, - // add custom element to the page - addCustomElement: (customElement: CustomElement) => { - const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) - dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - onClose(); - trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); - }, - // custom element search - findCustomElements: async (text?: string) => { - try { - await findCustomElements(); - } catch (err) { - notifyService.error(err, { - title: `Couldn't find custom elements`, - }); - } - }, - // remove custom element - removeCustomElement: async (id: string) => { - try { - await customElementService.remove(id); - await findCustomElements(); - } catch (err) { - notifyService.error(err, { - title: `Couldn't delete custom elements`, - }); - } - }, - // update custom element - updateCustomElement: async (id: string, name: string, description: string, image: string) => { - try { - await customElementService.update(id, { - name: camelCase(name), - displayName: name, - image, - help: description, - }); - await findCustomElements(); - } catch (err) { - notifyService.error(err, { - title: `Couldn't update custom elements`, - }); - } - }, - }; -}; - -export const SavedElementsModal = compose( - withServices, - withState('search', 'setSearch', ''), - withState('customElements', 'setCustomElements', []), - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx new file mode 100644 index 0000000000000..19e786edfd5fb --- /dev/null +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { camelCase } from 'lodash'; +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import { useNotifyService, useCustomElementService } from '../../services'; +// @ts-expect-error untyped local +import { selectToplevelNodes } from '../../state/actions/transient'; +// @ts-expect-error untyped local +import { insertNodes } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { + SavedElementsModal as Component, + Props as ComponentProps, +} from './saved_elements_modal.component'; +import { PositionedElement, CustomElement } from '../../../types'; + +const customElementAdded = 'elements-custom-added'; + +export type Props = Pick; + +export const SavedElementsModal = ({ onClose }: Props) => { + const notifyService = useNotifyService(); + const customElementService = useCustomElementService(); + const dispatch = useDispatch(); + const pageId = useSelector(getSelectedPage); + const [customElements, setCustomElements] = useState([]); + + const onSearch = async (search = '') => { + try { + const { customElements: foundElements } = await customElementService.find(search); + setCustomElements(foundElements); + } catch (err) { + notifyService.error(err, { + title: `Couldn't find custom elements`, + }); + } + }; + + const onAddCustomElement = (customElement: CustomElement) => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + + if (clonedNodes) { + dispatch(insertNodes(clonedNodes, pageId)); // first clone and persist the new node(s) + dispatch( + selectToplevelNodes( + clonedNodes + .filter((e: PositionedElement): boolean => !e.position.parent) + .map((e: PositionedElement): string => e.id) + ) + ); // then select the cloned node(s) + } + + onClose(); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); + }; + + const onRemoveCustomElement = async (id: string) => { + try { + await customElementService.remove(id); + await onSearch(); + } catch (err) { + notifyService.error(err, { + title: `Couldn't delete custom elements`, + }); + } + }; + + const onUpdateCustomElement = async ( + id: string, + name: string, + description: string, + image: string + ) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + await onSearch(); + } catch (err) { + notifyService.error(err, { + title: `Couldn't update custom elements`, + }); + } + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/plugins/canvas/public/lib/custom_element_service.ts deleted file mode 100644 index 6da624bb5d3ae..0000000000000 --- a/x-pack/plugins/canvas/public/lib/custom_element_service.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AxiosPromise } from 'axios'; -import { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib/constants'; -import { fetch } from '../../common/lib/fetch'; -import { CustomElement } from '../../types'; -import { pluginServices } from '../services'; - -const getApiPath = function () { - const basePath = pluginServices.getServices().platform.getBasePath(); - return `${basePath}${API_ROUTE_CUSTOM_ELEMENT}`; -}; - -export const create = (customElement: CustomElement): AxiosPromise => - fetch.post(getApiPath(), customElement); - -export const get = (customElementId: string): Promise => - fetch - .get(`${getApiPath()}/${customElementId}`) - .then(({ data: element }: { data: CustomElement }) => element); - -export const update = (id: string, element: Partial): AxiosPromise => - fetch.put(`${getApiPath()}/${id}`, element); - -export const remove = (id: string): AxiosPromise => fetch.delete(`${getApiPath()}/${id}`); - -export const find = async ( - searchTerm: string -): Promise<{ total: number; customElements: CustomElement[] }> => { - const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0; - - return fetch - .get(`${getApiPath()}/find?name=${validSearchTerm ? searchTerm : ''}&perPage=10000`) - .then( - ({ data: customElements }: { data: { total: number; customElements: CustomElement[] } }) => - customElements - ); -}; diff --git a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts index a46252081e672..62d405e167f1e 100644 --- a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts +++ b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts @@ -9,7 +9,6 @@ import { camelCase } from 'lodash'; import { getClipboardData, setClipboardData } from './clipboard'; import { cloneSubgraphs } from './clone_subgraphs'; import { pluginServices } from '../services'; -import * as customElementService from './custom_element_service'; import { getId } from './get_id'; import { PositionedElement } from '../../types'; import { ELEMENT_NUDGE_OFFSET, ELEMENT_SHIFT_OFFSET } from '../../common/lib/constants'; @@ -71,6 +70,7 @@ export const basicHandlerCreators = { image = '' ): void => { const notifyService = pluginServices.getServices().notify; + const customElementService = pluginServices.getServices().customElement; if (selectedNodes.length) { const content = JSON.stringify({ selectedNodes }); diff --git a/x-pack/plugins/canvas/public/services/custom_element.ts b/x-pack/plugins/canvas/public/services/custom_element.ts new file mode 100644 index 0000000000000..675a5a2f23c01 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/custom_element.ts @@ -0,0 +1,21 @@ +/* + * 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 { CustomElement } from '../../types'; + +export interface CustomElementFindResponse { + total: number; + customElements: CustomElement[]; +} + +export interface CanvasCustomElementService { + create: (customElement: CustomElement) => Promise; + get: (customElementId: string) => Promise; + update: (id: string, element: Partial) => Promise; + remove: (id: string) => Promise; + find: (searchTerm: string) => Promise; +} diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 07c1c3c253ff8..12afca2c5b8c7 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -8,18 +8,20 @@ export * from './legacy'; import { PluginServices } from '../../../../../src/plugins/presentation_util/public'; +import { CanvasEmbeddablesService } from './embeddables'; import { CanvasExpressionsService } from './expressions'; +import { CanvasCustomElementService } from './custom_element'; import { CanvasNavLinkService } from './nav_link'; -import { CanvasEmbeddablesService } from './embeddables'; import { CanvasNotifyService } from './notify'; import { CanvasPlatformService } from './platform'; import { CanvasReportingService } from './reporting'; import { CanvasWorkpadService } from './workpad'; export interface CanvasPluginServices { + customElement: CanvasCustomElementService; + embeddables: CanvasEmbeddablesService; expressions: CanvasExpressionsService; navLink: CanvasNavLinkService; - embeddables: CanvasEmbeddablesService; notify: CanvasNotifyService; platform: CanvasPlatformService; reporting: CanvasReportingService; @@ -28,11 +30,13 @@ export interface CanvasPluginServices { export const pluginServices = new PluginServices(); +export const useEmbeddablesService = () => + (() => pluginServices.getHooks().embeddables.useService())(); +export const useCustomElementService = () => + (() => pluginServices.getHooks().customElement.useService())(); export const useExpressionsService = () => (() => pluginServices.getHooks().expressions.useService())(); export const useNavLinkService = () => (() => pluginServices.getHooks().navLink.useService())(); -export const useEmbeddablesService = () => - (() => pluginServices.getHooks().embeddables.useService())(); export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())(); export const useReportingService = () => (() => pluginServices.getHooks().reporting.useService())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/custom_element.ts b/x-pack/plugins/canvas/public/services/kibana/custom_element.ts new file mode 100644 index 0000000000000..ec3b68d2d0bba --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana/custom_element.ts @@ -0,0 +1,41 @@ +/* + * 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 { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; + +import { API_ROUTE_CUSTOM_ELEMENT } from '../../../common/lib/constants'; +import { CustomElement } from '../../../types'; +import { CanvasStartDeps } from '../../plugin'; +import { CanvasCustomElementService } from '../custom_element'; + +export type CanvasCustomElementServiceFactory = KibanaPluginServiceFactory< + CanvasCustomElementService, + CanvasStartDeps +>; + +export const customElementServiceFactory: CanvasCustomElementServiceFactory = ({ coreStart }) => { + const { http } = coreStart; + const apiPath = `${API_ROUTE_CUSTOM_ELEMENT}`; + + return { + create: (customElement) => http.post(apiPath, { body: JSON.stringify(customElement) }), + get: (customElementId) => + http + .get(`${apiPath}/${customElementId}`) + .then(({ data: element }: { data: CustomElement }) => element), + update: (id, element) => http.put(`${apiPath}/${id}`, { body: JSON.stringify(element) }), + remove: (id) => http.delete(`${apiPath}/${id}`), + find: async (name) => { + return http.get(`${apiPath}/find`, { + query: { + name, + perPage: 10000, + }, + }); + }, + }; +}; diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index fb760fe4c183d..a756ca7b0d4d1 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -14,6 +14,7 @@ import { import { CanvasPluginServices } from '..'; import { CanvasStartDeps } from '../../plugin'; +import { customElementServiceFactory } from './custom_element'; import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; import { navLinkServiceFactory } from './nav_link'; @@ -22,8 +23,9 @@ import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; import { workpadServiceFactory } from './workpad'; -export { expressionsServiceFactory } from './expressions'; +export { customElementServiceFactory } from './custom_element'; export { embeddablesServiceFactory } from './embeddables'; +export { expressionsServiceFactory } from './expressions'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; @@ -33,9 +35,10 @@ export const pluginServiceProviders: PluginServiceProviders< CanvasPluginServices, KibanaPluginServiceParams > = { + customElement: new PluginServiceProvider(customElementServiceFactory), + embeddables: new PluginServiceProvider(embeddablesServiceFactory), expressions: new PluginServiceProvider(expressionsServiceFactory), navLink: new PluginServiceProvider(navLinkServiceFactory), - embeddables: new PluginServiceProvider(embeddablesServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), platform: new PluginServiceProvider(platformServiceFactory), reporting: new PluginServiceProvider(reportingServiceFactory), diff --git a/x-pack/plugins/canvas/public/services/stubs/custom_element.ts b/x-pack/plugins/canvas/public/services/stubs/custom_element.ts new file mode 100644 index 0000000000000..d30b5db36949a --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/custom_element.ts @@ -0,0 +1,21 @@ +/* + * 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 { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +import { CanvasCustomElementService } from '../custom_element'; + +type CanvasCustomElementServiceFactory = PluginServiceFactory; + +const noop = (..._args: any[]): any => {}; + +export const customElementServiceFactory: CanvasCustomElementServiceFactory = () => ({ + create: noop, + find: noop, + get: noop, + remove: noop, + update: noop, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 54bed79f61da3..df1370e83c00d 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -14,6 +14,7 @@ import { } from '../../../../../../src/plugins/presentation_util/public'; import { CanvasPluginServices } from '..'; +import { customElementServiceFactory } from './custom_element'; import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; import { navLinkServiceFactory } from './nav_link'; @@ -22,6 +23,7 @@ import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; import { workpadServiceFactory } from './workpad'; +export { customElementServiceFactory } from './custom_element'; export { expressionsServiceFactory } from './expressions'; export { navLinkServiceFactory } from './nav_link'; export { notifyServiceFactory } from './notify'; @@ -30,6 +32,7 @@ export { reportingServiceFactory } from './reporting'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders = { + customElement: new PluginServiceProvider(customElementServiceFactory), embeddables: new PluginServiceProvider(embeddablesServiceFactory), expressions: new PluginServiceProvider(expressionsServiceFactory), navLink: new PluginServiceProvider(navLinkServiceFactory), From 2d385b339dd334b05eca32c907a2072cc32f8b02 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Fri, 13 Aug 2021 12:19:16 -0500 Subject: [PATCH 61/91] [canvas] Fix setup server expressions cache; move to mount (#108473) --- x-pack/plugins/canvas/public/plugin.tsx | 3 +-- x-pack/plugins/canvas/public/setup_expressions.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 841f4b6d7c157..c149c67544865 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -95,8 +95,6 @@ export class CanvasPlugin })); } - setupExpressions({ coreSetup, setupPlugins }); - coreSetup.application.register({ category: DEFAULT_APP_CATEGORIES.kibana, id: 'canvas', @@ -108,6 +106,7 @@ export class CanvasPlugin const { CanvasSrcPlugin } = await import('../canvas_plugin_src/plugin'); const srcPlugin = new CanvasSrcPlugin(); srcPlugin.setup(coreSetup, { canvas: canvasApi }); + setupExpressions({ coreSetup, setupPlugins }); // Get start services const [coreStart, startPlugins] = await coreSetup.getStartServices(); diff --git a/x-pack/plugins/canvas/public/setup_expressions.ts b/x-pack/plugins/canvas/public/setup_expressions.ts index 3fc39564de0a9..e182d8efa097f 100644 --- a/x-pack/plugins/canvas/public/setup_expressions.ts +++ b/x-pack/plugins/canvas/public/setup_expressions.ts @@ -11,6 +11,8 @@ import { API_ROUTE_FUNCTIONS } from '../common/lib/constants'; import { CanvasSetupDeps } from './plugin'; +let cached: Promise | null = null; + // TODO: clintandrewhall - This is getting refactored shortly. https://github.com/elastic/kibana/issues/105675 export const setupExpressions = async ({ coreSetup, @@ -21,8 +23,6 @@ export const setupExpressions = async ({ }) => { const { expressions, bfetch } = setupPlugins; - let cached: Promise | null = null; - const loadServerFunctionWrappers = async () => { if (!cached) { cached = (async () => { From 560bd0b57b6aa687597eacad037bdcc381dcc8cd Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 13 Aug 2021 18:38:56 +0100 Subject: [PATCH 62/91] chore(NA): moving @kbn/es-archiver to babel transpiler (#108370) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-es-archiver/.babelrc | 3 +++ packages/kbn-es-archiver/BUILD.bazel | 25 +++++++++++++++++++------ packages/kbn-es-archiver/package.json | 4 ++-- packages/kbn-es-archiver/tsconfig.json | 3 ++- 4 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 packages/kbn-es-archiver/.babelrc diff --git a/packages/kbn-es-archiver/.babelrc b/packages/kbn-es-archiver/.babelrc new file mode 100644 index 0000000000000..7da72d1779128 --- /dev/null +++ b/packages/kbn-es-archiver/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-es-archiver/BUILD.bazel b/packages/kbn-es-archiver/BUILD.bazel index b7040b584a318..90c63f82b72fa 100644 --- a/packages/kbn-es-archiver/BUILD.bazel +++ b/packages/kbn-es-archiver/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-es-archiver" PKG_REQUIRE_NAME = "@kbn/es-archiver" @@ -27,7 +28,7 @@ NPM_MODULE_EXTRA_FILES = [ "package.json", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-dev-utils", "//packages/kbn-test", "//packages/kbn-utils", @@ -43,6 +44,13 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-test", + "//packages/kbn-utils", + "@npm//@elastic/elasticsearch", + "@npm//aggregate-error", + "@npm//globby", + "@npm//zlib", "@npm//@types/bluebird", "@npm//@types/chance", "@npm//@types/jest", @@ -52,7 +60,11 @@ TYPES_DEPS = [ "@npm//@types/sinon", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -64,13 +76,14 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -79,7 +92,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index e8eb7b5f8f1c9..0cce08eaf0352 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": "true", - "main": "target/index.js", - "types": "target/index.d.ts", + "main": "target_node/index.js", + "types": "target_types/index.d.ts", "kibana": { "devOnly": true } diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json index dce71fd6cd4a1..15c846f052b47 100644 --- a/packages/kbn-es-archiver/tsconfig.json +++ b/packages/kbn-es-archiver/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "outDir": "./target/types", "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-es-archiver/src", "types": [ From 79f1e186861112a8ff4854f7f1d516d8992a200b Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 13 Aug 2021 18:39:35 +0100 Subject: [PATCH 63/91] chore(NA): moving @kbn/io-ts-utils to babel transpiler (#108517) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-io-ts-utils/.babelrc | 3 +++ packages/kbn-io-ts-utils/BUILD.bazel | 22 ++++++++++++++----- packages/kbn-io-ts-utils/package.json | 4 ++-- packages/kbn-io-ts-utils/src/index.ts | 1 + packages/kbn-io-ts-utils/tsconfig.json | 6 +++-- .../src/create_router.test.tsx | 2 +- .../src/create_router.ts | 10 +++++++-- 7 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 packages/kbn-io-ts-utils/.babelrc diff --git a/packages/kbn-io-ts-utils/.babelrc b/packages/kbn-io-ts-utils/.babelrc new file mode 100644 index 0000000000000..7da72d1779128 --- /dev/null +++ b/packages/kbn-io-ts-utils/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index 0e5210bcc38a5..474fa2c2bb121 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-io-ts-utils" PKG_REQUIRE_NAME = "@kbn/io-ts-utils" @@ -24,7 +25,7 @@ NPM_MODULE_EXTRA_FILES = [ "package.json", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-config-schema", "@npm//fp-ts", "@npm//io-ts", @@ -33,12 +34,20 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "//packages/kbn-config-schema", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//tslib", "@npm//@types/jest", "@npm//@types/lodash", "@npm//@types/node", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -50,13 +59,14 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -65,7 +75,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json index 9d22277f27c01..fb1179b06bf45 100644 --- a/packages/kbn-io-ts-utils/package.json +++ b/packages/kbn-io-ts-utils/package.json @@ -1,7 +1,7 @@ { "name": "@kbn/io-ts-utils", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true diff --git a/packages/kbn-io-ts-utils/src/index.ts b/packages/kbn-io-ts-utils/src/index.ts index e94ac76b3c27b..88cfc063f738a 100644 --- a/packages/kbn-io-ts-utils/src/index.ts +++ b/packages/kbn-io-ts-utils/src/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +export { deepExactRt } from './deep_exact_rt'; export { jsonRt } from './json_rt'; export { mergeRt } from './merge_rt'; export { strictKeysRt } from './strict_keys_rt'; diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json index 3ee769739dfc7..72d1479621345 100644 --- a/packages/kbn-io-ts-utils/tsconfig.json +++ b/packages/kbn-io-ts-utils/tsconfig.json @@ -1,12 +1,14 @@ { "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "outDir": "./target/types", - "stripInternal": false, "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./target_types", + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-io-ts-utils/src", + "stripInternal": false, "types": [ "jest", "node" diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index fe82c48a33332..d8f42c8714e8b 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils/target/to_number_rt'; +import { toNumberRt } from '@kbn/io-ts-utils'; import { createRouter } from './create_router'; import { createMemoryHistory } from 'history'; import { route } from './route'; diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 5385eb44b747c..28f9e2774eb74 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -14,10 +14,16 @@ import { } from 'react-router-config'; import qs from 'query-string'; import { findLastIndex, merge, compact } from 'lodash'; -import { deepExactRt } from '@kbn/io-ts-utils/target/deep_exact_rt'; -import { mergeRt } from '@kbn/io-ts-utils/target/merge_rt'; +import type { deepExactRt as deepExactRtTyped, mergeRt as mergeRtTyped } from '@kbn/io-ts-utils'; +// @ts-expect-error +import { deepExactRt as deepExactRtNonTyped } from '@kbn/io-ts-utils/target_node/deep_exact_rt'; +// @ts-expect-error +import { mergeRt as mergeRtNonTyped } from '@kbn/io-ts-utils/target_node/merge_rt'; import { Route, Router } from './types'; +const deepExactRt: typeof deepExactRtTyped = deepExactRtNonTyped; +const mergeRt: typeof mergeRtTyped = mergeRtNonTyped; + export function createRouter(routes: TRoutes): Router { const routesByReactRouterConfig = new Map(); const reactRouterConfigsByRoute = new Map(); From 79eb426a8f34edc1a0e7a010c03d5ff40b81331d Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Fri, 13 Aug 2021 10:59:01 -0700 Subject: [PATCH 64/91] docs: Add anonymous auth to central config (#108285) --- docs/apm/agent-configuration.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index f2e07412c4a38..4e4a37067ea10 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -27,6 +27,8 @@ For this reason, it is still essential to set custom default configurations loca ==== APM Server setup This feature requires {apm-server-ref}/setup-kibana-endpoint.html[Kibana endpoint configuration] in APM Server. +In addition, if an APM agent is using {apm-server-ref}/configuration-anonymous.html[anonymous authentication] to communicate with the APM Server, +the agent's service name must be included in the `apm-server.auth.anonymous.allow_service` list. APM Server acts as a proxy between the agents and Kibana. Kibana communicates any changed settings to APM Server so that your agents only need to poll APM Server to determine which settings have changed. From c6dc6e207a8ed8d30d74f84b9a554b1af5c27d2c Mon Sep 17 00:00:00 2001 From: Apoorva Joshi <30438249+ajosh0504@users.noreply.github.com> Date: Fri, 13 Aug 2021 11:04:42 -0700 Subject: [PATCH 65/91] Adding host_risk_score_latest to the list of patterns to track for telemetry (#108547) * Adding host_risk_score_latest to the list of patterns to track for telemetry * Adding a test * Removing extra spaces at end of line- should make the linter happy --- .../telemetry_collection/get_data_telemetry/constants.ts | 3 +++ .../get_data_telemetry/get_data_telemetry.test.ts | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts index caa0c127d4eba..b5c2468b961b2 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts @@ -120,6 +120,9 @@ export const DATA_DATASETS_INDEX_PATTERNS = [ // meow attacks { pattern: '*meow*', patternName: 'meow' }, + + // experimental ml + { pattern: '*host_risk_score_latest', patternName: 'host_risk_score' }, ] as const; // Get the unique list of index patterns (some are duplicated for documentation purposes) diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts index dab1eaeed27ce..9fad1db9b6c3a 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -72,6 +72,8 @@ describe('get_data_telemetry', () => { { name: 'metricbeat-1234', docCount: 100, sizeInBytes: 10, isECS: false }, { name: '.app-search-1234', docCount: 0 }, { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name + { name: 'ml_host_risk_score_latest', docCount: 0 }, + { name: 'ml_host_risk_score', docCount: 0 }, // This should not match // New Indexing strategy: everything can be inferred from the constant_keyword values { name: '.ds-logs-nginx.access-default-000001', @@ -165,6 +167,11 @@ describe('get_data_telemetry', () => { index_count: 1, doc_count: 0, }, + { + pattern_name: 'host_risk_score', + index_count: 1, + doc_count: 0, + }, { data_stream: { dataset: 'nginx.access', type: 'logs' }, shipper: 'filebeat', From 505043898e3c452b883ad7326d63da50c70b036e Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 13 Aug 2021 13:38:57 -0600 Subject: [PATCH 66/91] [Maps] 'show this layer only' layer action (#107947) * [Maps] 'show this layer only' layer action * review feedback * remove ts code from js file Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../maps/public/actions/layer_actions.ts | 26 ++++ .../maps/public/classes/layers/layer.tsx | 5 + .../classes/layers/tile_layer/tile_layer.js | 4 + .../toc_entry_actions_popover.test.tsx.snap | 125 ++++++++++++++++++ .../toc_entry_actions_popover/index.ts | 6 + .../toc_entry_actions_popover.test.tsx | 13 ++ .../toc_entry_actions_popover.tsx | 16 +++ 7 files changed, 195 insertions(+) diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index eef325ca67cc5..edd21090143bf 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -246,6 +246,32 @@ export function toggleLayerVisible(layerId: string) { }; } +export function showThisLayerOnly(layerId: string) { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { + getLayerList(getState()).forEach((layer: ILayer, index: number) => { + if (layer.isBasemap(index)) { + return; + } + + // show target layer + if (layer.getId() === layerId) { + if (!layer.isVisible()) { + dispatch(setLayerVisibility(layerId, true)); + } + return; + } + + // hide all other layers + if (layer.isVisible()) { + dispatch(setLayerVisibility(layer.getId(), false)); + } + }); + }; +} + export function setSelectedLayer(layerId: string | null) { return async ( dispatch: ThunkDispatch, diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 5244882d41e03..472a796a6e7c9 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -106,6 +106,7 @@ export interface ILayer { getDescriptor(): LayerDescriptor; getGeoFieldNames(): string[]; getStyleMetaDescriptorFromLocalFeatures(): Promise; + isBasemap(order: number): boolean; } export type CustomIconAndTooltipContent = { @@ -527,4 +528,8 @@ export class AbstractLayer implements ILayer { async getStyleMetaDescriptorFromLocalFeatures(): Promise { return null; } + + isBasemap(): boolean { + return false; + } } diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js index 0995d117aaa47..d26c71ee9e215 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js @@ -117,4 +117,8 @@ export class TileLayer extends AbstractLayer { getLayerTypeIconName() { return 'grid'; } + + isBasemap(order) { + return order === 0; + } } diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index c7db60ae59ef9..6531b8a2f2501 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -555,3 +555,128 @@ exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1 /> `; + +exports[`TOCEntryActionsPopover should show "show this layer only" action when there are more then 2 layers 1`] = ` + + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="testLayer" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + , + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": , + "name": "Hide layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "showThisLayerOnlyButton", + "icon": , + "name": "Show this layer only", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerSettingsButton", + "disabled": false, + "icon": , + "name": "Edit layer settings", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "cloneLayerButton", + "icon": , + "name": "Clone layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "removeLayerButton", + "icon": , + "name": "Remove layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + size="m" + /> + +`; diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts index 55d15f7ed2410..400904b530963 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts @@ -14,9 +14,11 @@ import { fitToLayerExtent, removeLayer, setDrawMode, + showThisLayerOnly, toggleLayerVisible, updateEditLayer, } from '../../../../../../actions'; +import { getLayerListRaw } from '../../../../../../selectors/map_selectors'; import { getIsReadOnly } from '../../../../../../selectors/ui_selectors'; import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; import { DRAW_MODE } from '../../../../../../../common'; @@ -24,6 +26,7 @@ import { DRAW_MODE } from '../../../../../../../common'; function mapStateToProps(state: MapStoreState) { return { isReadOnly: getIsReadOnly(state), + numLayers: getLayerListRaw(state).length, }; } @@ -49,6 +52,9 @@ function mapDispatchToProps(dispatch: ThunkDispatch { + dispatch(showThisLayerOnly(layerId)); + }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx index 1610954399ae7..ae62b75400769 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx @@ -49,6 +49,8 @@ const defaultProps = { enablePointEditing: () => {}, openLayerSettings: () => {}, editModeActiveForLayer: false, + numLayers: 2, + showThisLayerOnly: () => {}, }; describe('TOCEntryActionsPopover', () => { @@ -114,4 +116,15 @@ describe('TOCEntryActionsPopover', () => { expect(component).toMatchSnapshot(); }); + + test('should show "show this layer only" action when there are more then 2 layers', async () => { + const component = shallow(); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index 2a3186f00d7ce..ed0946e526c80 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -34,9 +34,11 @@ export interface Props { isReadOnly: boolean; layer: ILayer; removeLayer: (layerId: string) => void; + showThisLayerOnly: (layerId: string) => void; supportsFitToBounds: boolean; toggleVisible: (layerId: string) => void; editModeActiveForLayer: boolean; + numLayers: number; } interface State { @@ -158,6 +160,20 @@ export class TOCEntryActionsPopover extends Component { }, }, ]; + if (this.props.numLayers > 2) { + actionItems.push({ + name: i18n.translate('xpack.maps.layerTocActions.showThisLayerOnlyTitle', { + defaultMessage: 'Show this layer only', + }), + icon: , + 'data-test-subj': 'showThisLayerOnlyButton', + toolTipContent: null, + onClick: () => { + this._closePopover(); + this.props.showThisLayerOnly(this.props.layer.getId()); + }, + }); + } actionItems.push({ disabled: this.props.isEditButtonDisabled, name: EDIT_LAYER_SETTINGS_LABEL, From c9220056442f3d83152b1ad180a7018189ee9dda Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 13 Aug 2021 13:41:17 -0600 Subject: [PATCH 67/91] Changes out cypress pipe (#108457) ## Summary Reduces flake by changing out a Cypress pipe for a `cy.wait`. This UI element does unusual things that make it unfit for Cypress pipe such as multiple clicks against it will cause the component to have a dialog appear and disappear with transition effects which can make pipe not able to click once when the click handler is present. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../cypress/tasks/alerts_detection_rules.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 6dbd1ae16c4ad..0e81f75a19046 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -110,13 +110,17 @@ export const deleteSelectedRules = () => { export const deleteRuleFromDetailsPage = () => { cy.get(ALL_ACTIONS).should('be.visible'); - cy.root() - .pipe(($el) => { - $el.find(ALL_ACTIONS).trigger('click'); - return $el.find(RULE_DETAILS_DELETE_BTN); - }) - .should(($el) => expect($el).to.be.visible); - cy.get(RULE_DETAILS_DELETE_BTN).pipe(($el) => $el.trigger('click')); + // We cannot use cy.root().pipe($el) withing this function and instead have to use a cy.wait() + // for the click handler to be registered. If you see flake here because of click handler issues + // increase the cy.wait(). The reason we cannot use cypress pipe is because multiple clicks on ALL_ACTIONS + // causes the pop up to show and then the next click for it to hide. Multiple clicks can cause + // the DOM to queue up and once we detect that the element is visible it can then become invisible later + cy.wait(1000); + cy.get(ALL_ACTIONS).click(); + cy.get(RULE_DETAILS_DELETE_BTN).should('be.visible'); + cy.get(RULE_DETAILS_DELETE_BTN) + .pipe(($el) => $el.trigger('click')) + .should(($el) => expect($el).to.be.not.visible); }; export const duplicateSelectedRules = () => { From 3b31ffc5fd2b0b47669b23fb4cc1c78d1f1607b6 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Fri, 13 Aug 2021 15:42:14 -0400 Subject: [PATCH 68/91] [Security Solution][Endpoint] Improve logic for determining count of pending actions and reenable pending status on UI (#108114) - Re-enable display of Pending isolation status on the UI along with a experimental feature flag to be able to turn it back off - Improves the logic around determining if an isolation action has actually been processed by the Endpoint by looking for an endpoint metadata update whose `event.created` timestamp is more recent than the timestamp on the isolation Action Response. The goal is to minimize/avoid the UX around isolation where a user might not see the result from the Endpoint (isolated or released) after having seen the pending status on the UI. - Add some protective code around our server side license watcher so that failures are not bubbled up and instead are logged to the kibana logs - Added new `findHostMetadataForFleetAgents()` method to the `EndpointMetadataService` - Added test mocks for `EndpointMetadataService` and tests for new method --- .../policy/get_policy_data_for_update.ts | 30 +++ .../common/endpoint/service/policy/index.ts | 8 + .../common/endpoint/types/actions.ts | 2 + .../common/endpoint/types/index.ts | 4 +- .../common/experimental_features.ts | 1 + .../endpoint_host_isolation_status.test.tsx | 27 ++- .../endpoint_host_isolation_status.tsx | 211 +++++++++--------- .../public/common/store/app/reducer.ts | 10 +- .../policy/store/policy_details/middleware.ts | 4 +- .../policy/store/policy_details/selectors.ts | 52 +---- .../endpoint/lib/policy/license_watch.ts | 69 +++--- .../endpoint/routes/actions/status.test.ts | 10 + .../server/endpoint/routes/actions/status.ts | 6 +- .../routes/metadata/query_builders.ts | 22 ++ .../server/endpoint/services/actions.ts | 132 ++++++++--- .../endpoint_metadata_service.test.ts | 69 ++++++ .../metadata/endpoint_metadata_service.ts | 32 ++- .../endpoint/services/metadata/mocks.ts | 45 ++++ .../signals/get_input_output_index.test.ts | 8 +- .../factory/hosts/details/helpers.ts | 6 +- .../factory/hosts/details/index.test.tsx | 8 +- 21 files changed, 508 insertions(+), 248 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/policy/get_policy_data_for_update.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/policy/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/service/policy/get_policy_data_for_update.ts b/x-pack/plugins/security_solution/common/endpoint/service/policy/get_policy_data_for_update.ts new file mode 100644 index 0000000000000..b929cde3dbb1c --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/policy/get_policy_data_for_update.ts @@ -0,0 +1,30 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { MaybeImmutable, NewPolicyData, PolicyData } from '../../types'; + +/** + * Given a Policy Data (package policy) object, return back a new object with only the field + * needed for an Update/Create API action + * @param policy + */ +export const getPolicyDataForUpdate = (policy: MaybeImmutable): NewPolicyData => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { id, revision, created_by, created_at, updated_by, updated_at, ...newPolicy } = policy; + // cast to `NewPolicyData` (mutable) since we cloned the entire object + const policyDataForUpdate = cloneDeep(newPolicy) as NewPolicyData; + const endpointPolicy = policyDataForUpdate.inputs[0].config.policy.value; + + // trim custom malware notification string + [ + endpointPolicy.windows.popup.malware, + endpointPolicy.mac.popup.malware, + ].forEach((objWithMessage) => objWithMessage.message.trim()); + + return policyDataForUpdate; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/policy/index.ts b/x-pack/plugins/security_solution/common/endpoint/service/policy/index.ts new file mode 100644 index 0000000000000..dc50d67197498 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/policy/index.ts @@ -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 * from './get_policy_data_for_update'; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 25fc831ca0aa4..be11ec3f99910 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -32,7 +32,9 @@ export interface EndpointActionResponse { action_id: string; /** The agent id that sent this action response */ agent_id: string; + /** timestamp when the action started to be processed by the Elastic Agent and/or Endpoint on the host */ started_at: string; + /** timestamp when the action completed processing by the Elastic Agent and/or Endpoint on the host */ completed_at: string; error?: string; action_data: EndpointActionData; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index dde7f7799757c..f398f1d57e600 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -6,7 +6,7 @@ */ import { ApplicationStart } from 'kibana/public'; -import { NewPackagePolicy, PackagePolicy } from '../../../../fleet/common'; +import { PackagePolicy, UpdatePackagePolicy } from '../../../../fleet/common'; import { ManifestSchema } from '../schema/manifest'; export * from './actions'; @@ -1020,7 +1020,7 @@ export type PolicyData = PackagePolicy & NewPolicyData; /** * New policy data. Used when updating the policy record via ingest APIs */ -export type NewPolicyData = NewPackagePolicy & { +export type NewPolicyData = UpdatePackagePolicy & { inputs: [ { type: 'endpoint'; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 0ae42d4baaec4..857aab10590e4 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -18,6 +18,7 @@ export const allowedExperimentalValues = Object.freeze({ trustedAppsByPolicyEnabled: false, excludePoliciesInFilterEnabled: false, uebaEnabled: false, + disableIsolationUIPendingStatuses: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx index 373b4d78a84cc..72b7e987436ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx @@ -16,9 +16,11 @@ describe('when using the EndpointHostIsolationStatus component', () => { let render: ( renderProps?: Partial ) => ReturnType; + let appContext: AppContextTestRender; beforeEach(() => { - const appContext = createAppRootMockRenderer(); + appContext = createAppRootMockRenderer(); + render = (renderProps = {}) => appContext.render( { expect(getByTestId('test').textContent).toBe('Isolated'); }); - // FIXME: un-skip when we bring back the pending isolation statuses - it.skip.each([ + it.each([ ['Isolating', { pendingIsolate: 2 }], ['Releasing', { pendingUnIsolate: 2 }], ['4 actions pending', { isIsolated: true, pendingUnIsolate: 2, pendingIsolate: 2 }], @@ -54,4 +55,24 @@ describe('when using the EndpointHostIsolationStatus component', () => { // Validate that the text color is set to `subdued` expect(getByTestId('test-pending').classList.contains('euiTextColor--subdued')).toBe(true); }); + + describe('and the disableIsolationUIPendingStatuses experimental feature flag is true', () => { + beforeEach(() => { + appContext.setExperimentalFlag({ disableIsolationUIPendingStatuses: true }); + }); + + it('should render `null` if not isolated', () => { + const renderResult = render({ pendingIsolate: 10, pendingUnIsolate: 20 }); + expect(renderResult.container.textContent).toBe(''); + }); + + it('should show `Isolated` when no pending actions and isolated', () => { + const { getByTestId } = render({ + isIsolated: true, + pendingIsolate: 10, + pendingUnIsolate: 20, + }); + expect(getByTestId('test').textContent).toBe('Isolated'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx index 425172a5cd460..6795557f17f1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx @@ -6,133 +6,140 @@ */ import React, { memo, useMemo } from 'react'; -import { EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTextColor, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -// import { useTestIdGenerator } from '../../../../management/components/hooks/use_test_id_generator'; +import { useTestIdGenerator } from '../../../../management/components/hooks/use_test_id_generator'; +import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; export interface EndpointHostIsolationStatusProps { isIsolated: boolean; /** the count of pending isolate actions */ pendingIsolate?: number; - /** the count of pending unisoalte actions */ + /** the count of pending unisolate actions */ pendingUnIsolate?: number; 'data-test-subj'?: string; } /** - * Component will display a host isoaltion status based on whether it is currently isolated or there are + * Component will display a host isolation status based on whether it is currently isolated or there are * isolate/unisolate actions pending. If none of these are applicable, no UI component will be rendered * (`null` is returned) */ export const EndpointHostIsolationStatus = memo( - ({ - isIsolated, - /* pendingIsolate = 0, pendingUnIsolate = 0,*/ 'data-test-subj': dataTestSubj, - }) => { - // const getTestId = useTestIdGenerator(dataTestSubj); + ({ isIsolated, pendingIsolate = 0, pendingUnIsolate = 0, 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const isPendingStatuseDisabled = useIsExperimentalFeatureEnabled( + 'disableIsolationUIPendingStatuses' + ); return useMemo(() => { + if (isPendingStatuseDisabled) { + // If nothing is pending and host is not currently isolated, then render nothing + if (!isIsolated) { + return null; + } + + return ( + + + + ); + } + // If nothing is pending and host is not currently isolated, then render nothing - if (!isIsolated) { + if (!isIsolated && !pendingIsolate && !pendingUnIsolate) { return null; } - // if (!isIsolated && !pendingIsolate && !pendingUnIsolate) { - // return null; - // } + // If nothing is pending, but host is isolated, then show isolation badge + if (!pendingIsolate && !pendingUnIsolate) { + return ( + + + + ); + } + + // If there are multiple types of pending isolation actions, then show count of actions with tooltip that displays breakdown + if (pendingIsolate && pendingUnIsolate) { + return ( + + +
+ +
+ + + + + {pendingIsolate} + + + + + + {pendingUnIsolate} + +
+ } + > + + + + + + ); + } + + // Show 'pending [un]isolate' depending on what's pending return ( - + + {pendingIsolate ? ( + + ) : ( + + )} + ); - - // If nothing is pending and host is not currently isolated, then render nothing - // if (!isIsolated && !pendingIsolate && !pendingUnIsolate) { - // return null; - // } - // - // // If nothing is pending, but host is isolated, then show isolation badge - // if (!pendingIsolate && !pendingUnIsolate) { - // return ( - // - // - // - // ); - // } - // - // // If there are multiple types of pending isolation actions, then show count of actions with tooltip that displays breakdown - // if (pendingIsolate && pendingUnIsolate) { - // return ( - // - // - //
- // - //
- // - // - // - // - // {pendingIsolate} - // - // - // - // - // - // {pendingUnIsolate} - // - //
- // } - // > - // - // - // - // - // - // ); - // } - // - // // Show 'pending [un]isolate' depending on what's pending - // return ( - // - // - // {pendingIsolate ? ( - // - // ) : ( - // - // )} - // - // - // ); - }, [dataTestSubj, isIsolated /* , getTestId , pendingIsolate, pendingUnIsolate*/]); + }, [ + dataTestSubj, + getTestId, + isIsolated, + isPendingStatuseDisabled, + pendingIsolate, + pendingUnIsolate, + ]); } ); diff --git a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts index 6ab572490f5d7..df07920526a9f 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts @@ -11,20 +11,14 @@ import { Note } from '../../lib/note'; import { addError, addErrorHash, addNotes, removeError, updateNote } from './actions'; import { AppModel, NotesById } from './model'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; export type AppState = AppModel; export const initialAppState: AppState = { notesById: {}, errors: [], - enableExperimental: { - trustedAppsByPolicyEnabled: false, - excludePoliciesInFilterEnabled: false, - metricsEntitiesEnabled: false, - ruleRegistryEnabled: false, - tGridEnabled: false, - uebaEnabled: false, - }, + enableExperimental: { ...allowedExperimentalValues }, }; interface UpdateNotesByIdParams { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts index 9e6bc4b0097f3..93c279db8a55b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts @@ -13,7 +13,6 @@ import { isOnPolicyDetailsPage, policyDetails, policyDetailsForUpdate, - getPolicyDataForUpdate, } from './selectors'; import { sendGetPackagePolicy, @@ -22,6 +21,7 @@ import { } from '../services/ingest'; import { NewPolicyData, PolicyData } from '../../../../../../common/endpoint/types'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; +import { getPolicyDataForUpdate } from '../../../../../../common/endpoint/service/policy/get_policy_data_for_update'; export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory = ( coreStart @@ -112,7 +112,7 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory) => state.policyItem; @@ -58,55 +59,6 @@ export const licensedPolicy: ( } ); -/** - * Given a Policy Data (package policy) object, return back a new object with only the field - * needed for an Update/Create API action - * @param policy - */ -export const getPolicyDataForUpdate = ( - policy: PolicyData | Immutable -): NewPolicyData | Immutable => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { id, revision, created_by, created_at, updated_by, updated_at, ...newPolicy } = policy; - - // trim custom malware notification string - return { - ...newPolicy, - inputs: (newPolicy as Immutable).inputs.map((input) => ({ - ...input, - config: input.config && { - ...input.config, - policy: { - ...input.config.policy, - value: { - ...input.config.policy.value, - windows: { - ...input.config.policy.value.windows, - popup: { - ...input.config.policy.value.windows.popup, - malware: { - ...input.config.policy.value.windows.popup.malware, - message: input.config.policy.value.windows.popup.malware.message.trim(), - }, - }, - }, - mac: { - ...input.config.policy.value.mac, - popup: { - ...input.config.policy.value.mac.popup, - malware: { - ...input.config.policy.value.mac.popup.malware, - message: input.config.policy.value.mac.popup.malware.message.trim(), - }, - }, - }, - }, - }, - }, - })), - }; -}; - /** * Return only the policy structure accepted for update/create */ @@ -114,7 +66,7 @@ export const policyDetailsForUpdate: ( state: Immutable ) => Immutable | undefined = createSelector(licensedPolicy, (policy) => { if (policy) { - return getPolicyDataForUpdate(policy); + return getPolicyDataForUpdate(policy) as Immutable; } }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index b8c6e57f72cea..63c5c85bd1e93 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -15,11 +15,7 @@ import { SavedObjectsClientContract, SavedObjectsServiceStart, } from 'src/core/server'; -import { - PackagePolicy, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, - UpdatePackagePolicy, -} from '../../../../../fleet/common'; +import { PackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../fleet/common'; import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { ILicense } from '../../../../../licensing/common/types'; import { @@ -27,6 +23,8 @@ import { unsetPolicyFeaturesAccordingToLicenseLevel, } from '../../../../common/license/policy_config'; import { LicenseService } from '../../../../common/license/license'; +import { PolicyData } from '../../../../common/endpoint/types'; +import { getPolicyDataForUpdate } from '../../../../common/endpoint/service/policy'; export class PolicyWatcher { private logger: Logger; @@ -83,6 +81,7 @@ export class PolicyWatcher { page: number; perPage: number; }; + do { try { response = await this.policyService.list(this.makeInternalSOClient(this.soStart), { @@ -96,33 +95,17 @@ export class PolicyWatcher { ); return; } - response.items.forEach(async (policy) => { - const updatePolicy: UpdatePackagePolicy = { - name: policy.name, - description: policy.description, - namespace: policy.namespace, - enabled: policy.enabled, - policy_id: policy.policy_id, - output_id: policy.output_id, - package: policy.package, - inputs: policy.inputs, - version: policy.version, - }; - const policyConfig = updatePolicy.inputs[0].config?.policy.value; - if (!isEndpointPolicyValidForLicense(policyConfig, license)) { - updatePolicy.inputs[0].config!.policy.value = unsetPolicyFeaturesAccordingToLicenseLevel( - policyConfig, - license - ); - try { - await this.policyService.update( - this.makeInternalSOClient(this.soStart), - this.esClient, - policy.id, - updatePolicy + + for (const policy of response.items as PolicyData[]) { + const updatePolicy = getPolicyDataForUpdate(policy); + const policyConfig = updatePolicy.inputs[0].config.policy.value; + + try { + if (!isEndpointPolicyValidForLicense(policyConfig, license)) { + updatePolicy.inputs[0].config.policy.value = unsetPolicyFeaturesAccordingToLicenseLevel( + policyConfig, + license ); - } catch (e) { - // try again for transient issues try { await this.policyService.update( this.makeInternalSOClient(this.soStart), @@ -130,14 +113,28 @@ export class PolicyWatcher { policy.id, updatePolicy ); - } catch (ee) { - this.logger.warn( - `Unable to remove platinum features from policy ${policy.id}: ${ee.message}` - ); + } catch (e) { + // try again for transient issues + try { + await this.policyService.update( + this.makeInternalSOClient(this.soStart), + this.esClient, + policy.id, + updatePolicy + ); + } catch (ee) { + this.logger.warn(`Unable to remove platinum features from policy ${policy.id}`); + this.logger.warn(ee); + } } } + } catch (error) { + this.logger.warn( + `Failure while attempting to verify Endpoint Policy features for policy [${policy.id}]` + ); + this.logger.warn(error); } - }); + } } while (response.page * response.perPage < response.total); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts index bc14dc7265071..facd53643bc4f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts @@ -243,6 +243,11 @@ describe('Endpoint Action Status', () => { ], [aMockResponse(actionID, mockAgentID)] ); + (endpointAppContextService.getEndpointMetadataService as jest.Mock) = jest + .fn() + .mockReturnValue({ + findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]), + }); const response = await getPendingStatus({ query: { agent_ids: [mockAgentID], @@ -273,6 +278,11 @@ describe('Endpoint Action Status', () => { ], [aMockResponse(actionTwoID, agentThree)] ); + (endpointAppContextService.getEndpointMetadataService as jest.Mock) = jest + .fn() + .mockReturnValue({ + findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]), + }); const response = await getPendingStatus({ query: { agent_ids: [agentOne, agentTwo, agentThree], diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts index ec03acee0335d..4ba03bf220c21 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts @@ -47,7 +47,11 @@ export const actionStatusRequestHandler = function ( ? [...new Set(req.query.agent_ids)] : [req.query.agent_ids]; - const response = await getPendingActionCounts(esClient, agentIDs); + const response = await getPendingActionCounts( + esClient, + endpointContext.service.getEndpointMetadataService(), + agentIDs + ); return res.ok({ body: { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index 99ec1d1022747..32ab80fb67684 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -166,6 +166,28 @@ export function getESQueryHostMetadataByID(agentID: string): estypes.SearchReque }; } +export function getESQueryHostMetadataByFleetAgentIds( + fleetAgentIds: string[] +): estypes.SearchRequest { + return { + body: { + query: { + bool: { + filter: [ + { + bool: { + should: [{ terms: { 'elastic.agent.id': fleetAgentIds } }], + }, + }, + ], + }, + }, + sort: MetadataSortMethod, + }, + index: metadataCurrentIndexPattern, + }; +} + export function getESQueryHostMetadataByIDs(agentIDs: string[]) { return { body: { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index 89f088e322ffa..80fb1c5d9c7b0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -14,6 +14,10 @@ import { EndpointActionResponse, EndpointPendingActions, } from '../../../common/endpoint/types'; +import { catchAndWrapError } from '../utils'; +import { EndpointMetadataService } from './metadata'; + +const PENDING_ACTION_RESPONSE_MAX_LAPSED_TIME = 300000; // 300k ms === 5 minutes export const getAuditLogResponse = async ({ elasticAgentId, @@ -173,6 +177,8 @@ const getActivityLog = async ({ export const getPendingActionCounts = async ( esClient: ElasticsearchClient, + metadataService: EndpointMetadataService, + /** The Fleet Agent IDs to be checked */ agentIDs: string[] ): Promise => { // retrieve the unexpired actions for the given hosts @@ -197,11 +203,60 @@ export const getPendingActionCounts = async ( }, { ignore: [404] } ) - .then((result) => result.body?.hits?.hits?.map((a) => a._source!) || []); + .then((result) => result.body?.hits?.hits?.map((a) => a._source!) || []) + .catch(catchAndWrapError); // retrieve any responses to those action IDs from these agents - const actionIDs = recentActions.map((a) => a.action_id); - const responses = await esClient + const responses = await fetchActionResponseIds( + esClient, + metadataService, + recentActions.map((a) => a.action_id), + agentIDs + ); + const pending: EndpointPendingActions[] = []; + + for (const agentId of agentIDs) { + const responseIDsFromAgent = responses[agentId]; + + pending.push({ + agent_id: agentId, + pending_actions: recentActions + .filter((a) => a.agents.includes(agentId) && !responseIDsFromAgent.includes(a.action_id)) + .map((a) => a.data.command) + .reduce((acc, cur) => { + if (cur in acc) { + acc[cur] += 1; + } else { + acc[cur] = 1; + } + return acc; + }, {} as EndpointPendingActions['pending_actions']), + }); + } + + return pending; +}; + +/** + * Returns back a map of elastic Agent IDs to array of Action IDs that have received a response. + * + * @param esClient + * @param metadataService + * @param actionIds + * @param agentIds + */ +const fetchActionResponseIds = async ( + esClient: ElasticsearchClient, + metadataService: EndpointMetadataService, + actionIds: string[], + agentIds: string[] +): Promise> => { + const actionResponsesByAgentId: Record = agentIds.reduce((acc, agentId) => { + acc[agentId] = []; + return acc; + }, {} as Record); + + const actionResponses = await esClient .search( { index: AGENT_ACTIONS_RESULTS_INDEX, @@ -211,8 +266,8 @@ export const getPendingActionCounts = async ( query: { bool: { filter: [ - { terms: { action_id: actionIDs } }, // get results for these actions - { terms: { agent_id: agentIDs } }, // ignoring responses from agents we're not looking for + { terms: { action_id: actionIds } }, // get results for these actions + { terms: { agent_id: agentIds } }, // ONLY responses for the agents we are interested in (ignore others) ], }, }, @@ -220,28 +275,49 @@ export const getPendingActionCounts = async ( }, { ignore: [404] } ) - .then((result) => result.body?.hits?.hits?.map((a) => a._source!) || []); - - // respond with action-count per agent - const pending: EndpointPendingActions[] = agentIDs.map((aid) => { - const responseIDsFromAgent = responses - .filter((r) => r.agent_id === aid) - .map((r) => r.action_id); - return { - agent_id: aid, - pending_actions: recentActions - .filter((a) => a.agents.includes(aid) && !responseIDsFromAgent.includes(a.action_id)) - .map((a) => a.data.command) - .reduce((acc, cur) => { - if (cur in acc) { - acc[cur] += 1; - } else { - acc[cur] = 1; - } - return acc; - }, {} as EndpointPendingActions['pending_actions']), - }; - }); + .then((result) => result.body?.hits?.hits?.map((a) => a._source!) || []) + .catch(catchAndWrapError); - return pending; + if (actionResponses.length === 0) { + return actionResponsesByAgentId; + } + + // Get the latest docs from the metadata datastream for the Elastic Agent IDs in the action responses + // This will be used determine if we should withhold the action id from the returned list in cases where + // the Endpoint might not yet have sent an updated metadata document (which would be representative of + // the state of the endpoint post-action) + const latestEndpointMetadataDocs = await metadataService.findHostMetadataForFleetAgents( + esClient, + agentIds + ); + + // Object of Elastic Agent Ids to event created date + const endpointLastEventCreated: Record = latestEndpointMetadataDocs.reduce( + (acc, endpointMetadata) => { + acc[endpointMetadata.elastic.agent.id] = new Date(endpointMetadata.event.created); + return acc; + }, + {} as Record + ); + + for (const actionResponse of actionResponses) { + const lastEndpointMetadataEventTimestamp = endpointLastEventCreated[actionResponse.agent_id]; + const actionCompletedAtTimestamp = new Date(actionResponse.completed_at); + // If enough time has lapsed in checking for updated Endpoint metadata doc so that we don't keep + // checking it forever. + // It uses the `@timestamp` in order to ensure we are looking at times that were set by the server + const enoughTimeHasLapsed = + Date.now() - new Date(actionResponse['@timestamp']).getTime() > + PENDING_ACTION_RESPONSE_MAX_LAPSED_TIME; + + if ( + !lastEndpointMetadataEventTimestamp || + enoughTimeHasLapsed || + lastEndpointMetadataEventTimestamp > actionCompletedAtTimestamp + ) { + actionResponsesByAgentId[actionResponse.agent_id].push(actionResponse.action_id); + } + } + + return actionResponsesByAgentId; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts new file mode 100644 index 0000000000000..05c7c618f58c1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { + createEndpointMetadataServiceTestContextMock, + EndpointMetadataServiceTestContextMock, +} from './mocks'; +import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; +import { createV2SearchResponse } from '../../routes/metadata/support/test_support'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { getESQueryHostMetadataByFleetAgentIds } from '../../routes/metadata/query_builders'; +import { EndpointError } from '../../errors'; +import { HostMetadata } from '../../../../common/endpoint/types'; + +describe('EndpointMetadataService', () => { + let testMockedContext: EndpointMetadataServiceTestContextMock; + let metadataService: EndpointMetadataServiceTestContextMock['endpointMetadataService']; + let esClient: ElasticsearchClientMock; + + beforeEach(() => { + testMockedContext = createEndpointMetadataServiceTestContextMock(); + metadataService = testMockedContext.endpointMetadataService; + esClient = elasticsearchServiceMock.createScopedClusterClient().asInternalUser; + }); + + describe('#findHostMetadataForFleetAgents()', () => { + let fleetAgentIds: string[]; + let endpointMetadataDoc: HostMetadata; + + beforeEach(() => { + fleetAgentIds = ['one', 'two']; + endpointMetadataDoc = new EndpointDocGenerator().generateHostMetadata(); + esClient.search.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise( + createV2SearchResponse(endpointMetadataDoc) + ) + ); + }); + + it('should call elasticsearch with proper filter', async () => { + await metadataService.findHostMetadataForFleetAgents(esClient, fleetAgentIds); + expect(esClient.search).toHaveBeenCalledWith( + { ...getESQueryHostMetadataByFleetAgentIds(fleetAgentIds), size: fleetAgentIds.length }, + { ignore: [404] } + ); + }); + + it('should throw a wrapped elasticsearch Error when one occurs', async () => { + esClient.search.mockRejectedValue(new Error('foo bar')); + await expect( + metadataService.findHostMetadataForFleetAgents(esClient, fleetAgentIds) + ).rejects.toThrow(EndpointError); + }); + + it('should return an array of Host Metadata documents', async () => { + const response = await metadataService.findHostMetadataForFleetAgents( + esClient, + fleetAgentIds + ); + expect(response).toEqual([endpointMetadataDoc]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts index 92a0391492a3f..5e2f46aa4c285 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts @@ -24,8 +24,14 @@ import { FleetAgentNotFoundError, FleetAgentPolicyNotFoundError, } from './errors'; -import { getESQueryHostMetadataByID } from '../../routes/metadata/query_builders'; -import { queryResponseToHostResult } from '../../routes/metadata/support/query_strategies'; +import { + getESQueryHostMetadataByFleetAgentIds, + getESQueryHostMetadataByID, +} from '../../routes/metadata/query_builders'; +import { + queryResponseToHostListResult, + queryResponseToHostResult, +} from '../../routes/metadata/support/query_strategies'; import { catchAndWrapError, DEFAULT_ENDPOINT_HOST_STATUS, @@ -62,7 +68,7 @@ export class EndpointMetadataService { * * @private */ - public get DANGEROUS_INTERNAL_SO_CLIENT() { + private get DANGEROUS_INTERNAL_SO_CLIENT() { // The INTERNAL SO client must be created during the first time its used. This is because creating it during // instance initialization (in `constructor(){}`) causes the SO Client to be invalid (perhaps because this // instantiation is happening during the plugin's the start phase) @@ -95,6 +101,26 @@ export class EndpointMetadataService { throw new EndpointHostNotFoundError(`Endpoint with id ${endpointId} not found`); } + /** + * Find a list of Endpoint Host Metadata document associated with a given list of Fleet Agent Ids + * @param esClient + * @param fleetAgentIds + */ + async findHostMetadataForFleetAgents( + esClient: ElasticsearchClient, + fleetAgentIds: string[] + ): Promise { + const query = getESQueryHostMetadataByFleetAgentIds(fleetAgentIds); + + query.size = fleetAgentIds.length; + + const searchResult = await esClient + .search(query, { ignore: [404] }) + .catch(catchAndWrapError); + + return queryResponseToHostListResult(searchResult.body).resultList; + } + /** * Retrieve a single endpoint host metadata along with fleet information * diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts new file mode 100644 index 0000000000000..147d8e11b567c --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts @@ -0,0 +1,45 @@ +/* + * 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 { SavedObjectsServiceStart } from 'kibana/server'; +import { EndpointMetadataService } from './endpoint_metadata_service'; +import { savedObjectsServiceMock } from '../../../../../../../src/core/server/mocks'; +import { + createMockAgentPolicyService, + createMockAgentService, +} from '../../../../../fleet/server/mocks'; +import { AgentPolicyServiceInterface, AgentService } from '../../../../../fleet/server'; + +/** + * Endpoint Metadata Service test context. Includes an instance of `EndpointMetadataService` along with the + * dependencies that were used to initialize that instance. + */ +export interface EndpointMetadataServiceTestContextMock { + savedObjectsStart: jest.Mocked; + agentService: jest.Mocked; + agentPolicyService: jest.Mocked; + endpointMetadataService: EndpointMetadataService; +} + +export const createEndpointMetadataServiceTestContextMock = ( + savedObjectsStart: jest.Mocked = savedObjectsServiceMock.createStartContract(), + agentService: jest.Mocked = createMockAgentService(), + agentPolicyService: jest.Mocked = createMockAgentPolicyService() +): EndpointMetadataServiceTestContextMock => { + const endpointMetadataService = new EndpointMetadataService( + savedObjectsStart, + agentService, + agentPolicyService + ); + + return { + savedObjectsStart, + agentService, + agentPolicyService, + endpointMetadataService, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts index c8ef0093291d5..787c26871d869 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -8,6 +8,7 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { getInputIndex, GetInputIndex } from './get_input_output_index'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; describe('get_input_output_index', () => { let servicesMock: AlertServicesMock; @@ -33,12 +34,7 @@ describe('get_input_output_index', () => { version: '8.0.0', index: ['test-input-index-1'], experimentalFeatures: { - trustedAppsByPolicyEnabled: false, - excludePoliciesInFilterEnabled: false, - metricsEntitiesEnabled: false, - ruleRegistryEnabled: false, - tGridEnabled: false, - uebaEnabled: false, + ...allowedExperimentalValues, }, }; }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 9b9f49a167397..c96e0040fd23d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -209,7 +209,11 @@ export const getHostEndpoint = async ( // Get Agent Status agentService.getAgentStatusById(esClient.asCurrentUser, fleetAgentId), // Get a list of pending actions (if any) - getPendingActionCounts(esClient.asCurrentUser, [fleetAgentId]).then((results) => { + getPendingActionCounts( + esClient.asCurrentUser, + endpointContext.service.getEndpointMetadataService(), + [fleetAgentId] + ).then((results) => { return results[0].pending_actions; }), ]); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index e921e8420eb96..7729934123899 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -18,6 +18,7 @@ import { } from '../../../../../../../../../src/core/server'; import { EndpointAppContext } from '../../../../../endpoint/types'; import { EndpointAppContextService } from '../../../../../endpoint/endpoint_app_context_services'; +import { allowedExperimentalValues } from '../../../../../../common/experimental_features'; const mockDeps = { esClient: {} as IScopedClusterClient, @@ -30,12 +31,7 @@ const mockDeps = { }, config: jest.fn().mockResolvedValue({}), experimentalFeatures: { - trustedAppsByPolicyEnabled: false, - excludePoliciesInFilterEnabled: false, - metricsEntitiesEnabled: false, - ruleRegistryEnabled: false, - tGridEnabled: false, - uebaEnabled: false, + ...allowedExperimentalValues, }, service: {} as EndpointAppContextService, } as EndpointAppContext, From 2fb785de64b6d562ec35ba67af9d39f2db7b4530 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 13 Aug 2021 13:11:35 -0700 Subject: [PATCH 69/91] [Reporting/Mgmt] Fix the missing deprecation warning under job status (#108484) * [Reporting/Mgmt] Fix the missing deprecation warning under job status * improve unit test * add space before the text and update snapshots --- x-pack/plugins/reporting/public/lib/job.tsx | 101 +++++---- .../report_listing.test.tsx.snap | 192 ++++++++++++------ .../public/management/report_listing.test.tsx | 183 ++++++++++++++++- .../reporting_management/report_listing.ts | 2 +- 4 files changed, 366 insertions(+), 112 deletions(-) diff --git a/x-pack/plugins/reporting/public/lib/job.tsx b/x-pack/plugins/reporting/public/lib/job.tsx index 86d618c57379b..31205af340741 100644 --- a/x-pack/plugins/reporting/public/lib/job.tsx +++ b/x-pack/plugins/reporting/public/lib/job.tsx @@ -103,14 +103,31 @@ export class Job { }); } - if (smallMessage) { - return ( + let deprecatedMessage: React.ReactElement | undefined; + if (this.isDeprecated) { + deprecatedMessage = ( - {smallMessage} + {' '} + + {i18n.translate('xpack.reporting.jobStatusDetail.deprecatedText', { + defaultMessage: `This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.`, + })} + ); } + if (smallMessage) { + return ( + <> + + {smallMessage} + + {deprecatedMessage ? deprecatedMessage : null} + + ); + } + return null; } @@ -169,45 +186,45 @@ export class Job { } getWarnings() { - if (this.status !== FAILED) { - const warnings: string[] = []; - if (this.isDeprecated) { - warnings.push( - i18n.translate('xpack.reporting.jobWarning.exportTypeDeprecated', { - defaultMessage: - 'This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.', - }) - ); - } - if (this.csv_contains_formulas) { - warnings.push( - i18n.translate('xpack.reporting.jobWarning.csvContainsFormulas', { - defaultMessage: - 'Your CSV contains characters that spreadsheet applications might interpret as formulas.', - }) - ); - } - if (this.max_size_reached) { - warnings.push( - i18n.translate('xpack.reporting.jobWarning.maxSizeReachedTooltip', { - defaultMessage: 'Your search reached the max size and contains partial data.', - }) - ); - } - - if (this.warnings?.length) { - warnings.push(...this.warnings); - } - - if (warnings.length) { - return ( -
    - {warnings.map((w, i) => { - return
  • {w}
  • ; - })} -
- ); - } + const warnings: string[] = []; + if (this.isDeprecated) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.exportTypeDeprecated', { + defaultMessage: + 'This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana.', + }) + ); + } + + if (this.csv_contains_formulas) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.csvContainsFormulas', { + defaultMessage: + 'Your CSV contains characters that spreadsheet applications might interpret as formulas.', + }) + ); + } + if (this.max_size_reached) { + warnings.push( + i18n.translate('xpack.reporting.jobWarning.maxSizeReachedTooltip', { + defaultMessage: 'Your search reached the max size and contains partial data.', + }) + ); + } + + // warnings could contain the failure message + if (this.status !== FAILED && this.warnings?.length) { + warnings.push(...this.warnings); + } + + if (warnings.length) { + return ( +
    + {warnings.map((w, i) => { + return
  • {w}
  • ; + })} +
+ ); } } diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index 926ca6e0b53dc..352c4dddb9f32 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -614,8 +614,8 @@ exports[`ReportListing Report job listing with some items 1`] = ` "index": ".reporting-2020.04.12", "isDeprecated": false, "jobtype": "printable_pdf", - "kibana_id": undefined, - "kibana_name": undefined, + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { "height": 720, @@ -660,8 +660,8 @@ exports[`ReportListing Report job listing with some items 1`] = ` "index": ".reporting-2020.04.12", "isDeprecated": false, "jobtype": "printable_pdf", - "kibana_id": undefined, - "kibana_name": undefined, + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { "height": 720, @@ -832,8 +832,8 @@ exports[`ReportListing Report job listing with some items 1`] = ` "index": ".reporting-2020.04.12", "isDeprecated": false, "jobtype": "printable_pdf", - "kibana_id": undefined, - "kibana_name": undefined, + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { "height": 720, @@ -1033,8 +1033,8 @@ exports[`ReportListing Report job listing with some items 1`] = ` "index": ".reporting-2020.04.12", "isDeprecated": false, "jobtype": "printable_pdf", - "kibana_id": undefined, - "kibana_name": undefined, + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { "height": 720, @@ -1234,8 +1234,8 @@ exports[`ReportListing Report job listing with some items 1`] = ` "index": ".reporting-2020.04.12", "isDeprecated": false, "jobtype": "printable_pdf", - "kibana_id": undefined, - "kibana_name": undefined, + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", + "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { "height": 720, @@ -7821,6 +7821,43 @@ exports[`ReportListing Report job listing with some items 1`] = ` + +
+ + + See report info for warnings. + + +
+
+ +
+ + + + This is a deprecated export type. Automation of this report will need to be re-created for compatibility with future versions of Kibana. + + +
+
@@ -7865,7 +7902,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "csv_contains_formulas": undefined, "id": "k905zdw11d34cbae0c3y6tzh", "index": ".reporting-2020.04.12", - "isDeprecated": false, + "isDeprecated": true, "jobtype": "printable_pdf", "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", @@ -7911,7 +7948,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "csv_contains_formulas": undefined, "id": "k905zdw11d34cbae0c3y6tzh", "index": ".reporting-2020.04.12", - "isDeprecated": false, + "isDeprecated": true, "jobtype": "printable_pdf", "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", @@ -8083,7 +8120,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "csv_contains_formulas": undefined, "id": "k905zdw11d34cbae0c3y6tzh", "index": ".reporting-2020.04.12", - "isDeprecated": false, + "isDeprecated": true, "jobtype": "printable_pdf", "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", @@ -8145,7 +8182,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -8331,7 +8368,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "csv_contains_formulas": undefined, "id": "k905zdw11d34cbae0c3y6tzh", "index": ".reporting-2020.04.12", - "isDeprecated": false, + "isDeprecated": true, "jobtype": "printable_pdf", "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", @@ -8532,7 +8569,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` "csv_contains_formulas": undefined, "id": "k905zdw11d34cbae0c3y6tzh", "index": ".reporting-2020.04.12", - "isDeprecated": false, + "isDeprecated": true, "jobtype": "printable_pdf", "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", @@ -8594,7 +8631,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -8606,16 +8643,16 @@ exports[`ReportListing Report job listing with some items 1`] = ` >
@@ -8907,17 +8981,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "created_by": "elastic", "csv_contains_formulas": undefined, "id": "k8t4ylcb07mi9d006214ifyg", - "index": ".reporting-2020.04.05", - "isDeprecated": false, + "index": ".reporting-2020.04.12", + "isDeprecated": true, "jobtype": "PNG", - "kibana_id": "f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { - "height": 1575, - "width": 1423, + "height": 720, + "width": 1080, }, - "id": "png", + "id": "preserve_layout", }, "max_attempts": 1, "max_size_reached": undefined, @@ -8953,17 +9027,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "created_by": "elastic", "csv_contains_formulas": undefined, "id": "k8t4ylcb07mi9d006214ifyg", - "index": ".reporting-2020.04.05", - "isDeprecated": false, + "index": ".reporting-2020.04.12", + "isDeprecated": true, "jobtype": "PNG", - "kibana_id": "f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { - "height": 1575, - "width": 1423, + "height": 720, + "width": 1080, }, - "id": "png", + "id": "preserve_layout", }, "max_attempts": 1, "max_size_reached": undefined, @@ -9125,17 +9199,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "created_by": "elastic", "csv_contains_formulas": undefined, "id": "k8t4ylcb07mi9d006214ifyg", - "index": ".reporting-2020.04.05", - "isDeprecated": false, + "index": ".reporting-2020.04.12", + "isDeprecated": true, "jobtype": "PNG", - "kibana_id": "f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { - "height": 1575, - "width": 1423, + "height": 720, + "width": 1080, }, - "id": "png", + "id": "preserve_layout", }, "max_attempts": 1, "max_size_reached": undefined, @@ -9188,7 +9262,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -9373,17 +9447,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "created_by": "elastic", "csv_contains_formulas": undefined, "id": "k8t4ylcb07mi9d006214ifyg", - "index": ".reporting-2020.04.05", - "isDeprecated": false, + "index": ".reporting-2020.04.12", + "isDeprecated": true, "jobtype": "PNG", - "kibana_id": "f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { - "height": 1575, - "width": 1423, + "height": 720, + "width": 1080, }, - "id": "png", + "id": "preserve_layout", }, "max_attempts": 1, "max_size_reached": undefined, @@ -9574,17 +9648,17 @@ exports[`ReportListing Report job listing with some items 1`] = ` "created_by": "elastic", "csv_contains_formulas": undefined, "id": "k8t4ylcb07mi9d006214ifyg", - "index": ".reporting-2020.04.05", - "isDeprecated": false, + "index": ".reporting-2020.04.12", + "isDeprecated": true, "jobtype": "PNG", - "kibana_id": "f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914", + "kibana_id": "5b2de169-2785-441b-ae8c-186a1936b17d", "kibana_name": "spicy.local", "layout": Object { "dimensions": Object { - "height": 1575, - "width": 1423, + "height": 720, + "width": 1080, }, - "id": "png", + "id": "preserve_layout", }, "max_attempts": 1, "max_size_reached": undefined, @@ -9637,7 +9711,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -9649,16 +9723,16 @@ exports[`ReportListing Report job listing with some items 1`] = ` > + + + ); + }; + + const onIsModifiedChange = jest.fn(); + const isFormModified = () => + onIsModifiedChange.mock.calls[onIsModifiedChange.mock.calls.length - 1][0]; + + const setup = registerTestBed(TestComp, { + defaultProps: { onIsModifiedChange }, + memoryRouter: { wrapComponent: false }, + }); + + test('should return true **only** when the field value differs from its initial value', async () => { + const { form } = await setup(); + + expect(isFormModified()).toBe(false); + + await act(async () => { + form.setInputValue('nameField', 'changed'); + }); + + expect(isFormModified()).toBe(true); + + // Put back to the initial value --> isModified should be false + await act(async () => { + form.setInputValue('nameField', 'initialValue'); + }); + expect(isFormModified()).toBe(false); + }); + + test('should accepts a list of field to discard', async () => { + const { form } = await setup({ discard: ['toDiscard'] }); + + expect(isFormModified()).toBe(false); + + await act(async () => { + form.setInputValue('toDiscardField', 'changed'); + }); + + // It should still not be modififed + expect(isFormModified()).toBe(false); + }); + + test('should take into account if a field is removed from the DOM **and** it existed on the form "defaultValue"', async () => { + const { find } = await setup(); + + expect(isFormModified()).toBe(false); + + await act(async () => { + find('hideNameButton').simulate('click'); + }); + expect(isFormModified()).toBe(true); + + // Put back the name + await act(async () => { + find('hideNameButton').simulate('click'); + }); + expect(isFormModified()).toBe(false); + + // Hide the lastname which is **not** in the form defaultValue + // this it won't set the form isModified to true + await act(async () => { + find('hideLastNameButton').simulate('click'); + }); + expect(isFormModified()).toBe(false); + }); +}); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_is_modified.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_is_modified.ts new file mode 100644 index 0000000000000..d87c44e614c04 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_is_modified.ts @@ -0,0 +1,97 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { useMemo } from 'react'; +import { get } from 'lodash'; + +import { FieldHook, FormHook } from '../types'; +import { useFormContext } from '../form_context'; +import { useFormData } from './use_form_data'; + +interface Options { + form?: FormHook; + /** List of field paths to discard when checking if a field has been modified */ + discard?: string[]; +} + +/** + * Hook to detect if any of the form fields have been modified by the user. + * If a field is modified and then the value is changed back to the initial value + * the form **won't be marked as modified**. + * This is useful to detect if a form has changed and we need to display a confirm modal + * to the user before he navigates away and loses his changes. + * + * @param options - Optional options object + * @returns flag to indicate if the form has been modified + */ +export const useFormIsModified = ({ + form: formFromOptions, + discard = [], +}: Options = {}): boolean => { + // As hook calls can not be conditional we first try to access the form through context + let form = useFormContext({ throwIfNotFound: false }); + + if (formFromOptions) { + form = formFromOptions; + } + + if (!form) { + throw new Error( + `useFormIsModified() used outside the form context and no form was provided in the options.` + ); + } + + const { getFields, __getFieldsRemoved, __getFormDefaultValue } = form; + + const discardToString = JSON.stringify(discard); + + // Create a map of the fields to discard to optimize look up + const fieldsToDiscard = useMemo(() => { + if (discard.length === 0) { + return; + } + + return discard.reduce((acc, path) => ({ ...acc, [path]: {} }), {} as { [key: string]: {} }); + + // discardToString === discard, we don't want to add it to the deps so we + // the coansumer does not need to memoize the array he provides. + }, [discardToString]); // eslint-disable-line react-hooks/exhaustive-deps + + // We listen to all the form data change to trigger a re-render + // and update our derived "isModified" state + useFormData({ form }); + + let predicate: (arg: [string, FieldHook]) => boolean = () => true; + + if (fieldsToDiscard) { + predicate = ([path]) => fieldsToDiscard[path] === undefined; + } + + let isModified = Object.entries(getFields()) + .filter(predicate) + .some(([_, field]) => field.isModified); + + if (isModified) { + return isModified; + } + + // Check if any field has been removed. + // If somme field has been removed **and** they were originaly present on the + // form "defaultValue" then the form has been modified. + const formDefaultValue = __getFormDefaultValue(); + const fieldOnFormDefaultValue = (path: string) => Boolean(get(formDefaultValue, path)); + + const fieldsRemovedFromDOM: string[] = fieldsToDiscard + ? Object.keys(__getFieldsRemoved()) + .filter((path) => fieldsToDiscard[path] === undefined) + .filter(fieldOnFormDefaultValue) + : Object.keys(__getFieldsRemoved()).filter(fieldOnFormDefaultValue); + + isModified = fieldsRemovedFromDOM.length > 0; + + return isModified; +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts index 72dbea3b14cce..19121bb6753a0 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -// Only export the useForm hook. The "useField" hook is for internal use -// as the consumer of the library must use the component -export { useForm, useFormData } from './hooks'; +// We don't export the "useField" hook as it is for internal use. +// The consumer of the library must use the component to create a field +export { useForm, useFormData, useFormIsModified } from './hooks'; export { getFieldValidityAndErrorMessage } from './helpers'; export * from './form_context'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 4e9ff29f0cdd3..151adea30c4f1 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -62,6 +62,8 @@ export interface FormHook __updateFormDataAt: (field: string, value: unknown) => void; __updateDefaultValueAt: (field: string, value: unknown) => void; __readFieldConfigFromSchema: (field: string) => FieldConfig; + __getFormDefaultValue: () => FormData; + __getFieldsRemoved: () => FieldsMap; } export type FormSchema = { @@ -109,6 +111,8 @@ export interface FieldHook { readonly errors: ValidationError[]; readonly isValid: boolean; readonly isPristine: boolean; + readonly isDirty: boolean; + readonly isModified: boolean; readonly isValidating: boolean; readonly isValidated: boolean; readonly isChangingValue: boolean; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.helpers.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.helpers.ts new file mode 100644 index 0000000000000..0d58b2ce89358 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.helpers.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { act } from 'react-dom/test-utils'; +import { registerTestBed, TestBed } from '@kbn/test/jest'; + +import { Context } from '../../public/components/field_editor_context'; +import { FieldEditor, Props } from '../../public/components/field_editor/field_editor'; +import { WithFieldEditorDependencies, getCommonActions } from './helpers'; + +export const defaultProps: Props = { + onChange: jest.fn(), + syntaxError: { + error: null, + clear: () => {}, + }, +}; + +export type FieldEditorTestBed = TestBed & { actions: ReturnType }; + +export const setup = async (props?: Partial, deps?: Partial) => { + let testBed: TestBed; + + await act(async () => { + testBed = await registerTestBed(WithFieldEditorDependencies(FieldEditor, deps), { + memoryRouter: { + wrapComponent: false, + }, + })({ ...defaultProps, ...props }); + }); + testBed!.component.update(); + + const actions = { + ...getCommonActions(testBed!), + }; + + return { ...testBed!, actions }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx similarity index 72% rename from src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx rename to src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx index dfea1a94de7fa..4a4c42f69fc8e 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx @@ -5,65 +5,25 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import React, { useState, useMemo } from 'react'; import { act } from 'react-dom/test-utils'; - -import '../../test_utils/setup_environment'; -import { registerTestBed, TestBed, getCommonActions } from '../../test_utils'; -import { RuntimeFieldPainlessError } from '../../lib'; -import { Field } from '../../types'; -import { FieldEditor, Props, FieldEditorFormState } from './field_editor'; -import { docLinksServiceMock } from '../../../../../core/public/mocks'; - -const defaultProps: Props = { - onChange: jest.fn(), - links: docLinksServiceMock.createStartContract() as any, - ctx: { - existingConcreteFields: [], - namesNotAllowed: [], - fieldTypeToProcess: 'runtime', - }, - indexPattern: { fields: [] } as any, - fieldFormatEditors: { - getAll: () => [], - getById: () => undefined, - }, - fieldFormats: {} as any, - uiSettings: {} as any, - syntaxError: { - error: null, - clear: () => {}, - }, -}; - -const setup = (props?: Partial) => { - const testBed = registerTestBed(FieldEditor, { - memoryRouter: { - wrapComponent: false, - }, - })({ ...defaultProps, ...props }) as TestBed; - - const actions = { - ...getCommonActions(testBed), - }; - - return { - ...testBed, - actions, - }; -}; +import { registerTestBed, TestBed } from '@kbn/test/jest'; + +// This import needs to come first as it contains the jest.mocks +import { setupEnvironment, getCommonActions, WithFieldEditorDependencies } from './helpers'; +import { + FieldEditor, + FieldEditorFormState, + Props, +} from '../../public/components/field_editor/field_editor'; +import type { Field } from '../../public/types'; +import type { RuntimeFieldPainlessError } from '../../public/lib'; +import { setup, FieldEditorTestBed, defaultProps } from './field_editor.helpers'; describe('', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); + const { server, httpRequestsMockHelpers } = setupEnvironment(); - let testBed: TestBed & { actions: ReturnType }; + let testBed: FieldEditorTestBed; let onChange: jest.Mock = jest.fn(); const lastOnChangeCall = (): FieldEditorFormState[] => @@ -104,12 +64,22 @@ describe('', () => { return formState!; }; - beforeEach(() => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { onChange = jest.fn(); + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] }); }); - test('initial state should have "set custom label", "set value" and "set format" turned off', () => { - testBed = setup(); + test('initial state should have "set custom label", "set value" and "set format" turned off', async () => { + testBed = await setup(); ['customLabel', 'value', 'format'].forEach((row) => { const testSubj = `${row}Row.toggle`; @@ -132,7 +102,7 @@ describe('', () => { script: { source: 'emit("hello")' }, }; - testBed = setup({ onChange, field }); + testBed = await setup({ onChange, field }); expect(onChange).toHaveBeenCalled(); @@ -153,25 +123,22 @@ describe('', () => { describe('validation', () => { test('should accept an optional list of existing fields and prevent creating duplicates', async () => { const existingFields = ['myRuntimeField']; - testBed = setup({ - onChange, - ctx: { + testBed = await setup( + { + onChange, + }, + { namesNotAllowed: existingFields, existingConcreteFields: [], fieldTypeToProcess: 'runtime', - }, - }); + } + ); const { form, component, actions } = testBed; - await act(async () => { - actions.toggleFormRow('value'); - }); - - await act(async () => { - form.setInputValue('nameField.input', existingFields[0]); - form.setInputValue('scriptField', 'echo("hello")'); - }); + await actions.toggleFormRow('value'); + await actions.fields.updateName(existingFields[0]); + await actions.fields.updateScript('echo("hello")'); await act(async () => { jest.advanceTimersByTime(1000); // Make sure our debounced error message is in the DOM @@ -192,20 +159,23 @@ describe('', () => { script: { source: 'emit("hello"' }, }; - testBed = setup({ - field, - onChange, - ctx: { + testBed = await setup( + { + field, + onChange, + }, + { namesNotAllowed: existingRuntimeFieldNames, existingConcreteFields: [], fieldTypeToProcess: 'runtime', - }, - }); + } + ); const { form, component } = testBed; const lastState = getLastStateUpdate(); await submitFormAndGetData(lastState); component.update(); + expect(getLastStateUpdate().isValid).toBe(true); expect(form.getErrorsMessages()).toEqual([]); }); @@ -217,13 +187,14 @@ describe('', () => { script: { source: 'emit(6)' }, }; - const TestComponent = () => { - const dummyError = { - reason: 'Awwww! Painless syntax error', - message: '', - position: { offset: 0, start: 0, end: 0 }, - scriptStack: [''], - }; + const dummyError = { + reason: 'Awwww! Painless syntax error', + message: '', + position: { offset: 0, start: 0, end: 0 }, + scriptStack: [''], + }; + + const ComponentToProvidePainlessSyntaxErrors = () => { const [error, setError] = useState(null); const clearError = useMemo(() => () => setError(null), []); const syntaxError = useMemo(() => ({ error, clear: clearError }), [error, clearError]); @@ -240,22 +211,29 @@ describe('', () => { ); }; - const customTestbed = registerTestBed(TestComponent, { - memoryRouter: { - wrapComponent: false, - }, - })() as TestBed; + let testBedToCapturePainlessErrors: TestBed; + + await act(async () => { + testBedToCapturePainlessErrors = await registerTestBed( + WithFieldEditorDependencies(ComponentToProvidePainlessSyntaxErrors), + { + memoryRouter: { + wrapComponent: false, + }, + } + )(); + }); testBed = { - ...customTestbed, - actions: getCommonActions(customTestbed), + ...testBedToCapturePainlessErrors!, + actions: getCommonActions(testBedToCapturePainlessErrors!), }; const { form, component, find, - actions: { changeFieldType }, + actions: { fields }, } = testBed; // We set some dummy painless error @@ -267,7 +245,7 @@ describe('', () => { expect(form.getErrorsMessages()).toEqual(['Awwww! Painless syntax error']); // We change the type and expect the form error to not be there anymore - await changeFieldType('keyword'); + await fields.updateType('keyword'); expect(form.getErrorsMessages()).toEqual([]); }); }); diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.helpers.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.helpers.ts new file mode 100644 index 0000000000000..5b916c1cd9960 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.helpers.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { act } from 'react-dom/test-utils'; +import { registerTestBed, TestBed } from '@kbn/test/jest'; + +import { Context } from '../../public/components/field_editor_context'; +import { + FieldEditorFlyoutContent, + Props, +} from '../../public/components/field_editor_flyout_content'; +import { WithFieldEditorDependencies, getCommonActions } from './helpers'; + +const defaultProps: Props = { + onSave: () => {}, + onCancel: () => {}, + runtimeFieldValidator: () => Promise.resolve(null), + isSavingField: false, +}; + +const getActions = (testBed: TestBed) => { + return { + ...getCommonActions(testBed), + }; +}; + +export const setup = async (props?: Partial, deps?: Partial) => { + let testBed: TestBed; + + // Setup testbed + await act(async () => { + testBed = await registerTestBed(WithFieldEditorDependencies(FieldEditorFlyoutContent, deps), { + memoryRouter: { + wrapComponent: false, + }, + })({ ...defaultProps, ...props }); + }); + + testBed!.component.update(); + + return { ...testBed!, actions: getActions(testBed!) }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts similarity index 66% rename from src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts rename to src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts index ed71e40fc80a9..9b00ff762fe8f 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts @@ -7,58 +7,30 @@ */ import { act } from 'react-dom/test-utils'; -import '../test_utils/setup_environment'; -import { registerTestBed, TestBed, noop, getCommonActions } from '../test_utils'; - -import { FieldEditor } from './field_editor'; -import { FieldEditorFlyoutContent, Props } from './field_editor_flyout_content'; -import { docLinksServiceMock } from '../../../../core/public/mocks'; - -const defaultProps: Props = { - onSave: noop, - onCancel: noop, - docLinks: docLinksServiceMock.createStartContract() as any, - FieldEditor, - indexPattern: { fields: [] } as any, - uiSettings: {} as any, - fieldFormats: {} as any, - fieldFormatEditors: {} as any, - fieldTypeToProcess: 'runtime', - runtimeFieldValidator: () => Promise.resolve(null), - isSavingField: false, -}; - -const setup = (props: Props = defaultProps) => { - const testBed = registerTestBed(FieldEditorFlyoutContent, { - memoryRouter: { wrapComponent: false }, - })(props) as TestBed; - - const actions = { - ...getCommonActions(testBed), - }; - - return { - ...testBed, - actions, - }; -}; +import type { Props } from '../../public/components/field_editor_flyout_content'; +import { setupEnvironment } from './helpers'; +import { setup } from './field_editor_flyout_content.helpers'; describe('', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + beforeAll(() => { + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['foo'] }); jest.useFakeTimers(); }); afterAll(() => { jest.useRealTimers(); + server.restore(); }); - test('should have the correct title', () => { - const { exists, find } = setup(); + test('should have the correct title', async () => { + const { exists, find } = await setup(); expect(exists('flyoutTitle')).toBe(true); expect(find('flyoutTitle').text()).toBe('Create field'); }); - test('should allow a field to be provided', () => { + test('should allow a field to be provided', async () => { const field = { name: 'foo', type: 'ip', @@ -67,7 +39,7 @@ describe('', () => { }, }; - const { find } = setup({ ...defaultProps, field }); + const { find } = await setup({ field }); expect(find('flyoutTitle').text()).toBe(`Edit field 'foo'`); expect(find('nameField.input').props().value).toBe(field.name); @@ -83,7 +55,7 @@ describe('', () => { }; const onSave: jest.Mock = jest.fn(); - const { find } = setup({ ...defaultProps, onSave, field }); + const { find } = await setup({ onSave, field }); await act(async () => { find('fieldSaveButton').simulate('click'); @@ -100,9 +72,9 @@ describe('', () => { expect(fieldReturned).toEqual(field); }); - test('should accept an onCancel prop', () => { + test('should accept an onCancel prop', async () => { const onCancel = jest.fn(); - const { find } = setup({ ...defaultProps, onCancel }); + const { find } = await setup({ onCancel }); find('closeFlyoutButton').simulate('click'); @@ -113,7 +85,7 @@ describe('', () => { test('should validate the fields and prevent saving invalid form', async () => { const onSave: jest.Mock = jest.fn(); - const { find, exists, form, component } = setup({ ...defaultProps, onSave }); + const { find, exists, form, component } = await setup({ onSave }); expect(find('fieldSaveButton').props().disabled).toBe(false); @@ -139,20 +111,12 @@ describe('', () => { const { find, - component, - form, - actions: { toggleFormRow, changeFieldType }, - } = setup({ ...defaultProps, onSave }); - - act(() => { - form.setInputValue('nameField.input', 'someName'); - toggleFormRow('value'); - }); - component.update(); + actions: { toggleFormRow, fields }, + } = await setup({ onSave }); - await act(async () => { - form.setInputValue('scriptField', 'echo("hello")'); - }); + await fields.updateName('someName'); + await toggleFormRow('value'); + await fields.updateScript('echo("hello")'); await act(async () => { // Let's make sure that validation has finished running @@ -174,7 +138,7 @@ describe('', () => { }); // Change the type and make sure it is forwarded - await changeFieldType('other_type', 'Other type'); + await fields.updateType('other_type', 'Other type'); await act(async () => { find('fieldSaveButton').simulate('click'); diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.helpers.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.helpers.ts new file mode 100644 index 0000000000000..068ebce638aa1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.helpers.ts @@ -0,0 +1,185 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; +import { registerTestBed, TestBed } from '@kbn/test/jest'; + +import { API_BASE_PATH } from '../../common/constants'; +import { Context } from '../../public/components/field_editor_context'; +import { + FieldEditorFlyoutContent, + Props, +} from '../../public/components/field_editor_flyout_content'; +import { + WithFieldEditorDependencies, + getCommonActions, + spyIndexPatternGetAllFields, + spySearchQuery, + spySearchQueryResponse, +} from './helpers'; + +const defaultProps: Props = { + onSave: () => {}, + onCancel: () => {}, + runtimeFieldValidator: () => Promise.resolve(null), + isSavingField: false, +}; + +/** + * This handler lets us mock the fields present on the index pattern during our test + * @param fields The fields of the index pattern + */ +export const setIndexPatternFields = (fields: Array<{ name: string; displayName: string }>) => { + spyIndexPatternGetAllFields.mockReturnValue(fields); +}; + +export interface TestDoc { + title: string; + subTitle: string; + description: string; +} + +export const getSearchCallMeta = () => { + const totalCalls = spySearchQuery.mock.calls.length; + const lastCall = spySearchQuery.mock.calls[totalCalls - 1] ?? null; + let lastCallParams = null; + + if (lastCall) { + lastCallParams = lastCall[0]; + } + + return { + totalCalls, + lastCall, + lastCallParams, + }; +}; + +export const setSearchResponse = ( + documents: Array<{ _id: string; _index: string; _source: TestDoc }> +) => { + spySearchQueryResponse.mockResolvedValue({ + rawResponse: { + hits: { + total: documents.length, + hits: documents, + }, + }, + }); +}; + +const getActions = (testBed: TestBed) => { + const getWrapperRenderedIndexPatternFields = (): ReactWrapper | null => { + if (testBed.find('indexPatternFieldList').length === 0) { + return null; + } + return testBed.find('indexPatternFieldList.listItem'); + }; + + const getRenderedIndexPatternFields = (): Array<{ key: string; value: string }> => { + const allFields = getWrapperRenderedIndexPatternFields(); + + if (allFields === null) { + return []; + } + + return allFields.map((field) => { + const key = testBed.find('key', field).text(); + const value = testBed.find('value', field).text(); + return { key, value }; + }); + }; + + const getRenderedFieldsPreview = () => { + if (testBed.find('fieldPreviewItem').length === 0) { + return []; + } + + const previewFields = testBed.find('fieldPreviewItem.listItem'); + + return previewFields.map((field) => { + const key = testBed.find('key', field).text(); + const value = testBed.find('value', field).text(); + return { key, value }; + }); + }; + + const setFilterFieldsValue = async (value: string) => { + await act(async () => { + testBed.form.setInputValue('filterFieldsInput', value); + }); + + testBed.component.update(); + }; + + // Need to set "server: any" (instead of SinonFakeServer) to avoid a TS error :( + // Error: Exported variable 'setup' has or is using name 'Document' from external module "/dev/shm/workspace/parallel/14/kibana/node_modules/@types/sinon/ts3.1/index" + const getLatestPreviewHttpRequest = (server: any) => { + let i = server.requests.length - 1; + + while (i >= 0) { + const request = server.requests[i]; + if (request.method === 'POST' && request.url === `${API_BASE_PATH}/field_preview`) { + return { + ...request, + requestBody: JSON.parse(JSON.parse(request.requestBody).body), + }; + } + i--; + } + + throw new Error(`Can't access the latest preview HTTP request as it hasn't been called.`); + }; + + const goToNextDocument = async () => { + await act(async () => { + testBed.find('goToNextDocButton').simulate('click'); + }); + testBed.component.update(); + }; + + const goToPreviousDocument = async () => { + await act(async () => { + testBed.find('goToPrevDocButton').simulate('click'); + }); + testBed.component.update(); + }; + + const loadCustomDocument = (docId: string) => {}; + + return { + ...getCommonActions(testBed), + getWrapperRenderedIndexPatternFields, + getRenderedIndexPatternFields, + getRenderedFieldsPreview, + setFilterFieldsValue, + getLatestPreviewHttpRequest, + goToNextDocument, + goToPreviousDocument, + loadCustomDocument, + }; +}; + +export const setup = async (props?: Partial, deps?: Partial) => { + let testBed: TestBed; + + // Setup testbed + await act(async () => { + testBed = await registerTestBed(WithFieldEditorDependencies(FieldEditorFlyoutContent, deps), { + memoryRouter: { + wrapComponent: false, + }, + })({ ...defaultProps, ...props }); + }); + + testBed!.component.update(); + + return { ...testBed!, actions: getActions(testBed!) }; +}; + +export type FieldEditorFlyoutContentTestBed = TestBed & { actions: ReturnType }; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts new file mode 100644 index 0000000000000..65089bc24317b --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts @@ -0,0 +1,890 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment, fieldFormatsOptions, indexPatternNameForTest } from './helpers'; +import { + setup, + setIndexPatternFields, + getSearchCallMeta, + setSearchResponse, + FieldEditorFlyoutContentTestBed, + TestDoc, +} from './field_editor_flyout_preview.helpers'; +import { createPreviewError } from './helpers/mocks'; + +interface EsDoc { + _id: string; + _index: string; + _source: TestDoc; +} + +describe('Field editor Preview panel', () => { + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + let testBed: FieldEditorFlyoutContentTestBed; + + const mockDocuments: EsDoc[] = [ + { + _id: '001', + _index: 'testIndex', + _source: { + title: 'First doc - title', + subTitle: 'First doc - subTitle', + description: 'First doc - description', + }, + }, + { + _id: '002', + _index: 'testIndex', + _source: { + title: 'Second doc - title', + subTitle: 'Second doc - subTitle', + description: 'Second doc - description', + }, + }, + { + _id: '003', + _index: 'testIndex', + _source: { + title: 'Third doc - title', + subTitle: 'Third doc - subTitle', + description: 'Third doc - description', + }, + }, + ]; + + const [doc1, doc2, doc3] = mockDocuments; + + const indexPatternFields: Array<{ name: string; displayName: string }> = [ + { + name: 'title', + displayName: 'title', + }, + { + name: 'subTitle', + displayName: 'subTitle', + }, + { + name: 'description', + displayName: 'description', + }, + ]; + + beforeEach(async () => { + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] }); + setIndexPatternFields(indexPatternFields); + setSearchResponse(mockDocuments); + + testBed = await setup(); + }); + + test('should display the preview panel when either "set value" or "set format" is activated', async () => { + const { + exists, + actions: { toggleFormRow }, + } = testBed; + + expect(exists('previewPanel')).toBe(false); + + await toggleFormRow('value'); + expect(exists('previewPanel')).toBe(true); + + await toggleFormRow('value', 'off'); + expect(exists('previewPanel')).toBe(false); + + await toggleFormRow('format'); + expect(exists('previewPanel')).toBe(true); + + await toggleFormRow('format', 'off'); + expect(exists('previewPanel')).toBe(false); + }); + + test('should correctly set the title and subtitle of the panel', async () => { + const { + find, + actions: { toggleFormRow, fields, waitForUpdates }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await waitForUpdates(); + + expect(find('previewPanel.title').text()).toBe('Preview'); + expect(find('previewPanel.subTitle').text()).toBe(`From: ${indexPatternNameForTest}`); + }); + + test('should list the list of fields of the index pattern', async () => { + const { + actions: { toggleFormRow, fields, getRenderedIndexPatternFields, waitForUpdates }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await waitForUpdates(); + + expect(getRenderedIndexPatternFields()).toEqual([ + { + key: 'title', + value: mockDocuments[0]._source.title, + }, + { + key: 'subTitle', + value: mockDocuments[0]._source.subTitle, + }, + { + key: 'description', + value: mockDocuments[0]._source.description, + }, + ]); + }); + + test('should filter down the field in the list', async () => { + const { + exists, + find, + component, + actions: { + toggleFormRow, + fields, + setFilterFieldsValue, + getRenderedIndexPatternFields, + waitForUpdates, + }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await waitForUpdates(); + + // Should find a single field + await setFilterFieldsValue('descr'); + expect(getRenderedIndexPatternFields()).toEqual([ + { key: 'description', value: 'First doc - description' }, + ]); + + // Should be case insensitive + await setFilterFieldsValue('title'); + expect(exists('emptySearchResult')).toBe(false); + expect(getRenderedIndexPatternFields()).toEqual([ + { key: 'title', value: 'First doc - title' }, + { key: 'subTitle', value: 'First doc - subTitle' }, + ]); + + // Should display an empty search result with a button to clear + await setFilterFieldsValue('doesNotExist'); + expect(exists('emptySearchResult')).toBe(true); + expect(getRenderedIndexPatternFields()).toEqual([]); + expect(exists('emptySearchResult.clearSearchButton')); + + find('emptySearchResult.clearSearchButton').simulate('click'); + component.update(); + expect(getRenderedIndexPatternFields()).toEqual([ + { + key: 'title', + value: mockDocuments[0]._source.title, + }, + { + key: 'subTitle', + value: mockDocuments[0]._source.subTitle, + }, + { + key: 'description', + value: mockDocuments[0]._source.description, + }, + ]); + }); + + test('should pin the field to the top of the list', async () => { + const { + find, + component, + actions: { + toggleFormRow, + fields, + getWrapperRenderedIndexPatternFields, + getRenderedIndexPatternFields, + waitForUpdates, + }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await waitForUpdates(); + + const fieldsRendered = getWrapperRenderedIndexPatternFields(); + + if (fieldsRendered === null) { + throw new Error('No index pattern field rendered.'); + } + + expect(fieldsRendered.length).toBe(Object.keys(doc1._source).length); + // make sure that the last one if the "description" field + expect(fieldsRendered.at(2).text()).toBe('descriptionFirst doc - description'); + + // Click the third field in the list ("description") + const descriptionField = fieldsRendered.at(2); + find('pinFieldButton', descriptionField).simulate('click'); + component.update(); + + expect(getRenderedIndexPatternFields()).toEqual([ + { key: 'description', value: 'First doc - description' }, // Pinned! + { key: 'title', value: 'First doc - title' }, + { key: 'subTitle', value: 'First doc - subTitle' }, + ]); + }); + + describe('empty prompt', () => { + test('should display an empty prompt if no name and no script are defined', async () => { + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates }, + } = testBed; + + await toggleFormRow('value'); + expect(exists('previewPanel')).toBe(true); + expect(exists('previewPanel.emptyPrompt')).toBe(true); + + await fields.updateName('someName'); + await waitForUpdates(); + expect(exists('previewPanel.emptyPrompt')).toBe(false); + + await fields.updateName(' '); + await waitForUpdates(); + expect(exists('previewPanel.emptyPrompt')).toBe(true); + + // The name is empty and the empty prompt is displayed, let's now add a script... + await fields.updateScript('echo("hello")'); + await waitForUpdates(); + expect(exists('previewPanel.emptyPrompt')).toBe(false); + + await fields.updateScript(' '); + await waitForUpdates(); + expect(exists('previewPanel.emptyPrompt')).toBe(true); + }); + + test('should **not** display an empty prompt editing a document with a script', async () => { + const field = { + name: 'foo', + type: 'ip', + script: { + source: 'emit("hello world")', + }, + }; + + // We open the editor with a field to edit. The preview panel should be open + // and the empty prompt should not be there as we have a script and we'll load + // the preview. + await act(async () => { + testBed = await setup({ field }); + }); + + const { exists, component } = testBed; + component.update(); + + expect(exists('previewPanel')).toBe(true); + expect(exists('previewPanel.emptyPrompt')).toBe(false); + }); + + test('should **not** display an empty prompt editing a document with format defined', async () => { + const field = { + name: 'foo', + type: 'ip', + format: { + id: 'upper', + params: {}, + }, + }; + + // We open the editor with a field to edit. The preview panel should be open + // and the empty prompt should not be there as we have a script and we'll load + // the preview. + await act(async () => { + testBed = await setup({ field }); + }); + + const { exists, component } = testBed; + component.update(); + + expect(exists('previewPanel')).toBe(true); + expect(exists('previewPanel.emptyPrompt')).toBe(false); + }); + }); + + describe('key & value', () => { + test('should set an empty value when no script is provided', async () => { + const { + actions: { toggleFormRow, fields, getRenderedFieldsPreview, waitForUpdates }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await waitForUpdates(); + + expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: '-' }]); + }); + + test('should set the value returned by the painless _execute API', async () => { + const scriptEmitResponse = 'Field emit() response'; + httpRequestsMockHelpers.setFieldPreviewResponse({ values: [scriptEmitResponse] }); + + const { + actions: { + toggleFormRow, + fields, + waitForDocumentsAndPreviewUpdate, + getLatestPreviewHttpRequest, + getRenderedFieldsPreview, + }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await fields.updateScript('echo("hello")'); + await waitForDocumentsAndPreviewUpdate(); + const request = getLatestPreviewHttpRequest(server); + + // Make sure the payload sent is correct + expect(request.requestBody).toEqual({ + context: 'keyword_field', + document: { + description: 'First doc - description', + subTitle: 'First doc - subTitle', + title: 'First doc - title', + }, + index: 'testIndex', + script: { + source: 'echo("hello")', + }, + }); + + // And that we display the response + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'myRuntimeField', value: scriptEmitResponse }, + ]); + }); + + test('should display an updating indicator while fetching the preview', async () => { + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); + + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, + } = testBed; + + await toggleFormRow('value'); + await waitForUpdates(); // wait for docs to be fetched + expect(exists('isUpdatingIndicator')).toBe(false); + + await fields.updateScript('echo("hello")'); + expect(exists('isUpdatingIndicator')).toBe(true); + + await waitForDocumentsAndPreviewUpdate(); + expect(exists('isUpdatingIndicator')).toBe(false); + }); + + test('should not display the updating indicator when neither the type nor the script has changed', async () => { + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); + + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, + } = testBed; + + await toggleFormRow('value'); + await waitForUpdates(); // wait for docs to be fetched + await fields.updateName('myRuntimeField'); + await fields.updateScript('echo("hello")'); + expect(exists('isUpdatingIndicator')).toBe(true); + await waitForDocumentsAndPreviewUpdate(); + expect(exists('isUpdatingIndicator')).toBe(false); + + await fields.updateName('nameChanged'); + // We haven't changed the type nor the script so there should not be any updating indicator + expect(exists('isUpdatingIndicator')).toBe(false); + }); + + describe('read from _source', () => { + test('should display the _source value when no script is provided and the name matched one of the fields in _source', async () => { + const { + actions: { + toggleFormRow, + fields, + getRenderedFieldsPreview, + waitForDocumentsAndPreviewUpdate, + }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('subTitle'); + await waitForDocumentsAndPreviewUpdate(); + + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'subTitle', value: 'First doc - subTitle' }, + ]); + }); + + test('should display the value returned by the _execute API and fallback to _source if "Set value" is turned off', async () => { + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['valueFromExecuteAPI'] }); + + const { + actions: { toggleFormRow, fields, waitForUpdates, getRenderedFieldsPreview }, + } = testBed; + + await toggleFormRow('value'); + await waitForUpdates(); // fetch documents + await fields.updateName('description'); // Field name is a field in _source + await fields.updateScript('echo("hello")'); + await waitForUpdates(); // fetch preview + + // We render the value from the _execute API + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'description', value: 'valueFromExecuteAPI' }, + ]); + + await toggleFormRow('format', 'on'); + await toggleFormRow('value', 'off'); + + // Fallback to _source value when "Set value" is turned off and we have a format + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'description', value: 'First doc - description' }, + ]); + }); + }); + }); + + describe('format', () => { + test('should apply the format to the value', async () => { + /** + * Each of the formatter has already its own test. Here we are simply + * doing a smoke test to make sure that the preview panel applies the formatter + * to the runtime field value. + * We do that by mocking (in "setup_environment.tsx") the implementation of the + * the fieldFormats.getInstance() handler. + */ + const scriptEmitResponse = 'hello'; + httpRequestsMockHelpers.setFieldPreviewResponse({ values: [scriptEmitResponse] }); + + const { + actions: { + toggleFormRow, + fields, + waitForUpdates, + waitForDocumentsAndPreviewUpdate, + getRenderedFieldsPreview, + }, + } = testBed; + + await fields.updateName('myRuntimeField'); + await toggleFormRow('value'); + await fields.updateScript('echo("hello")'); + await waitForDocumentsAndPreviewUpdate(); + + // before + expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'hello' }]); + + // after + await toggleFormRow('format'); + await fields.updateFormat(fieldFormatsOptions[0].id); // select 'upper' format + await waitForUpdates(); + expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'HELLO' }]); + }); + }); + + describe('error handling', () => { + test('should display the error returned by the Painless _execute API', async () => { + const error = createPreviewError({ reason: 'Houston we got a problem' }); + httpRequestsMockHelpers.setFieldPreviewResponse({ values: [], error, status: 400 }); + + const { + exists, + find, + actions: { + toggleFormRow, + fields, + waitForUpdates, + waitForDocumentsAndPreviewUpdate, + getRenderedFieldsPreview, + }, + } = testBed; + + await fields.updateName('myRuntimeField'); + await toggleFormRow('value'); + await fields.updateScript('bad()'); + await waitForDocumentsAndPreviewUpdate(); + + expect(exists('fieldPreviewItem')).toBe(false); + expect(exists('indexPatternFieldList')).toBe(false); + expect(exists('previewError')).toBe(true); + expect(find('previewError.reason').text()).toBe(error.caused_by.reason); + + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); + await fields.updateScript('echo("ok")'); + await waitForUpdates(); + + expect(exists('fieldPreviewItem')).toBe(true); + expect(find('indexPatternFieldList.listItem').length).toBeGreaterThan(0); + expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'ok' }]); + }); + + test('should handle error when a document is not found', async () => { + const { + exists, + find, + form, + actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, + } = testBed; + + await fields.updateName('myRuntimeField'); + await toggleFormRow('value'); + await waitForDocumentsAndPreviewUpdate(); + + // We will return no document from the search + setSearchResponse([]); + + await act(async () => { + form.setInputValue('documentIdField', 'wrongID'); + }); + await waitForUpdates(); + + expect(exists('previewError')).toBe(true); + expect(find('previewError').text()).toContain('Document ID not found'); + expect(exists('isUpdatingIndicator')).toBe(false); + }); + }); + + describe('Cluster document load and navigation', () => { + const customLoadedDoc: EsDoc = { + _id: '123456', + _index: 'otherIndex', + _source: { + title: 'loaded doc - title', + subTitle: 'loaded doc - subTitle', + description: 'loaded doc - description', + }, + }; + + test('should update the field list when the document changes', async () => { + const { + actions: { + toggleFormRow, + fields, + getRenderedIndexPatternFields, + goToNextDocument, + goToPreviousDocument, + waitForUpdates, + }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await waitForUpdates(); + + expect(getRenderedIndexPatternFields()[0]).toEqual({ + key: 'title', + value: doc1._source.title, + }); + + await goToNextDocument(); + expect(getRenderedIndexPatternFields()[0]).toEqual({ + key: 'title', + value: doc2._source.title, + }); + + await goToNextDocument(); + expect(getRenderedIndexPatternFields()[0]).toEqual({ + key: 'title', + value: doc3._source.title, + }); + + // Going next we circle back to the first document of the list + await goToNextDocument(); + expect(getRenderedIndexPatternFields()[0]).toEqual({ + key: 'title', + value: doc1._source.title, + }); + + // Let's go backward + await goToPreviousDocument(); + expect(getRenderedIndexPatternFields()[0]).toEqual({ + key: 'title', + value: doc3._source.title, + }); + + await goToPreviousDocument(); + expect(getRenderedIndexPatternFields()[0]).toEqual({ + key: 'title', + value: doc2._source.title, + }); + }); + + test('should update the field preview value when the document changes', async () => { + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['valueDoc1'] }); + const { + actions: { + toggleFormRow, + fields, + waitForUpdates, + waitForDocumentsAndPreviewUpdate, + getRenderedFieldsPreview, + goToNextDocument, + }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await fields.updateScript('echo("hello world")'); + await waitForDocumentsAndPreviewUpdate(); + + expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'valueDoc1' }]); + + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['valueDoc2'] }); + await goToNextDocument(); + await waitForUpdates(); + + expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'valueDoc2' }]); + }); + + test('should load a custom document when an ID is passed', async () => { + const { + component, + form, + exists, + actions: { + toggleFormRow, + fields, + getRenderedIndexPatternFields, + getRenderedFieldsPreview, + waitForUpdates, + waitForDocumentsAndPreviewUpdate, + }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await fields.updateScript('echo("hello world")'); + await waitForDocumentsAndPreviewUpdate(); + + // First make sure that we have the original cluster data is loaded + // and the preview value rendered. + expect(getRenderedIndexPatternFields()[0]).toEqual({ + key: 'title', + value: doc1._source.title, + }); + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'myRuntimeField', value: 'mockedScriptValue' }, + ]); + + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['loadedDocPreview'] }); + setSearchResponse([customLoadedDoc]); + + await act(async () => { + form.setInputValue('documentIdField', '123456'); + }); + component.update(); + // We immediately remove the index pattern fields + expect(getRenderedIndexPatternFields()).toEqual([]); + + await waitForDocumentsAndPreviewUpdate(); + + expect(getRenderedIndexPatternFields()).toEqual([ + { + key: 'title', + value: 'loaded doc - title', + }, + { + key: 'subTitle', + value: 'loaded doc - subTitle', + }, + { + key: 'description', + value: 'loaded doc - description', + }, + ]); + + await waitForUpdates(); // Then wait for the preview HTTP request + + // The preview should have updated + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'myRuntimeField', value: 'loadedDocPreview' }, + ]); + + // The nav should not be there when loading a single document + expect(exists('documentsNav')).toBe(false); + // There should be a link to load back the cluster data + expect(exists('loadDocsFromClusterButton')).toBe(true); + }); + + test('should load back the cluster data after providing a custom ID', async () => { + const { + form, + component, + find, + actions: { + toggleFormRow, + fields, + getRenderedFieldsPreview, + getRenderedIndexPatternFields, + waitForUpdates, + waitForDocumentsAndPreviewUpdate, + }, + } = testBed; + + await toggleFormRow('value'); + await waitForUpdates(); // fetch documents + await fields.updateName('myRuntimeField'); + await fields.updateScript('echo("hello world")'); + await waitForUpdates(); // fetch preview + + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['loadedDocPreview'] }); + setSearchResponse([customLoadedDoc]); + + // Load a custom document ID + await act(async () => { + form.setInputValue('documentIdField', '123456'); + }); + await waitForDocumentsAndPreviewUpdate(); + + // Load back the cluster data + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['clusterDataDocPreview'] }); + setSearchResponse(mockDocuments); + + await act(async () => { + find('loadDocsFromClusterButton').simulate('click'); + }); + component.update(); + // We immediately remove the index pattern fields + expect(getRenderedIndexPatternFields()).toEqual([]); + + await waitForDocumentsAndPreviewUpdate(); + + // The preview should be updated with the cluster data preview + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'myRuntimeField', value: 'clusterDataDocPreview' }, + ]); + }); + + test('should not lose the state of single document vs cluster data after displaying the empty prompt', async () => { + const { + form, + component, + exists, + actions: { + toggleFormRow, + fields, + getRenderedIndexPatternFields, + waitForDocumentsAndPreviewUpdate, + }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await waitForDocumentsAndPreviewUpdate(); + + // Initial state where we have the cluster data loaded and the doc navigation + expect(exists('documentsNav')).toBe(true); + expect(exists('loadDocsFromClusterButton')).toBe(false); + + setSearchResponse([customLoadedDoc]); + + await act(async () => { + form.setInputValue('documentIdField', '123456'); + }); + component.update(); + await waitForDocumentsAndPreviewUpdate(); + + expect(exists('documentsNav')).toBe(false); + expect(exists('loadDocsFromClusterButton')).toBe(true); + + // Clearing the name will display the empty prompt as we don't have any script + await fields.updateName(''); + expect(exists('previewPanel.emptyPrompt')).toBe(true); + + // Give another name to hide the empty prompt and show the preview panel back + await fields.updateName('newName'); + expect(exists('previewPanel.emptyPrompt')).toBe(false); + + // We should still display the single document state + expect(exists('documentsNav')).toBe(false); + expect(exists('loadDocsFromClusterButton')).toBe(true); + expect(getRenderedIndexPatternFields()[0]).toEqual({ + key: 'title', + value: 'loaded doc - title', + }); + }); + + test('should send the correct params to the data plugin search() handler', async () => { + const { + form, + component, + find, + actions: { toggleFormRow, fields, waitForUpdates }, + } = testBed; + + const expectedParamsToFetchClusterData = { + params: { index: 'testIndexPattern', body: { size: 50 } }, + }; + + // Initial state + let searchMeta = getSearchCallMeta(); + const initialCount = searchMeta.totalCalls; + + // Open the preview panel. This will trigger document fetchint + await fields.updateName('myRuntimeField'); + await toggleFormRow('value'); + await waitForUpdates(); + + searchMeta = getSearchCallMeta(); + expect(searchMeta.totalCalls).toBe(initialCount + 1); + expect(searchMeta.lastCallParams).toEqual(expectedParamsToFetchClusterData); + + // Load single doc + setSearchResponse([customLoadedDoc]); + const nextId = '123456'; + await act(async () => { + form.setInputValue('documentIdField', nextId); + }); + component.update(); + await waitForUpdates(); + + searchMeta = getSearchCallMeta(); + expect(searchMeta.totalCalls).toBe(initialCount + 2); + expect(searchMeta.lastCallParams).toEqual({ + params: { + body: { + query: { + ids: { + values: [nextId], + }, + }, + size: 1, + }, + index: 'testIndexPattern', + }, + }); + + // Back to cluster data + setSearchResponse(mockDocuments); + await act(async () => { + find('loadDocsFromClusterButton').simulate('click'); + }); + searchMeta = getSearchCallMeta(); + expect(searchMeta.totalCalls).toBe(initialCount + 3); + expect(searchMeta.lastCallParams).toEqual(expectedParamsToFetchClusterData); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/common_actions.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/common_actions.ts new file mode 100644 index 0000000000000..ca061968dae20 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/common_actions.ts @@ -0,0 +1,111 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { act } from 'react-dom/test-utils'; +import { TestBed } from '@kbn/test/jest'; + +export const getCommonActions = (testBed: TestBed) => { + const toggleFormRow = async ( + row: 'customLabel' | 'value' | 'format', + value: 'on' | 'off' = 'on' + ) => { + const testSubj = `${row}Row.toggle`; + const toggle = testBed.find(testSubj); + const isOn = toggle.props()['aria-checked']; + + if ((value === 'on' && isOn) || (value === 'off' && isOn === false)) { + return; + } + + await act(async () => { + testBed.form.toggleEuiSwitch(testSubj); + }); + + testBed.component.update(); + }; + + // Fields + const updateName = async (value: string) => { + await act(async () => { + testBed.form.setInputValue('nameField.input', value); + }); + + testBed.component.update(); + }; + + const updateScript = async (value: string) => { + await act(async () => { + testBed.form.setInputValue('scriptField', value); + }); + + testBed.component.update(); + }; + + const updateType = async (value: string, label?: string) => { + await act(async () => { + testBed.find('typeField').simulate('change', [ + { + value, + label: label ?? value, + }, + ]); + }); + + testBed.component.update(); + }; + + const updateFormat = async (value: string, label?: string) => { + await act(async () => { + testBed.find('editorSelectedFormatId').simulate('change', { target: { value } }); + }); + + testBed.component.update(); + }; + + /** + * Allows us to bypass the debounce time of 500ms before updating the preview. We also simulate + * a 2000ms latency when searching ES documents (see setup_environment.tsx). + */ + const waitForUpdates = async () => { + await act(async () => { + jest.runAllTimers(); + }); + + testBed.component.update(); + }; + + /** + * When often need to both wait for the documents to be fetched and + * the preview to be fetched. We can't increase the `jest.advanceTimersByTime` time + * as those are 2 different operations that occur in sequence. + */ + const waitForDocumentsAndPreviewUpdate = async () => { + // Wait for documents to be fetched + await act(async () => { + jest.runAllTimers(); + }); + + // Wait for preview to update + await act(async () => { + jest.runAllTimers(); + }); + + testBed.component.update(); + }; + + return { + toggleFormRow, + waitForUpdates, + waitForDocumentsAndPreviewUpdate, + fields: { + updateName, + updateType, + updateScript, + updateFormat, + }, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/http_requests.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/http_requests.ts new file mode 100644 index 0000000000000..4b03db247bad1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/http_requests.ts @@ -0,0 +1,47 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import sinon, { SinonFakeServer } from 'sinon'; +import { API_BASE_PATH } from '../../../common/constants'; + +type HttpResponse = Record | any[]; + +// Register helpers to mock HTTP Requests +const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const setFieldPreviewResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.body.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', `${API_BASE_PATH}/field_preview`, [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + + return { + setFieldPreviewResponse, + }; +}; + +export const init = () => { + const server = sinon.fakeServer.create(); + server.respondImmediately = true; + + // Define default response for unhandled requests. + // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, + // and we can mock them all with a 200 instead of mocking each one individually. + server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); + + const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + + return { + server, + httpRequestsMockHelpers, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/index.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/index.ts new file mode 100644 index 0000000000000..6a1f1aa74036a --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/index.ts @@ -0,0 +1,21 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { findTestSubject, TestBed } from '@kbn/test/jest'; + +export { + setupEnvironment, + WithFieldEditorDependencies, + spySearchQuery, + spySearchQueryResponse, + spyIndexPatternGetAllFields, + fieldFormatsOptions, + indexPatternNameForTest, +} from './setup_environment'; + +export { getCommonActions } from './common_actions'; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx similarity index 78% rename from src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx rename to src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx index 885bcc87f89df..e291ec7b4ca08 100644 --- a/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx @@ -5,44 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import React from 'react'; const EDITOR_ID = 'testEditor'; -jest.mock('../../../kibana_react/public', () => { - const original = jest.requireActual('../../../kibana_react/public'); - - /** - * We mock the CodeEditor because it requires the - * with the uiSettings passed down. Let's use a simple in our tests. - */ - const CodeEditorMock = (props: any) => { - // Forward our deterministic ID to the consumer - // We need below for the PainlessLang.getSyntaxErrors mock - props.editorDidMount({ - getModel() { - return { - id: EDITOR_ID, - }; - }, - }); - - return ( - ) => { - props.onChange(e.target.value); - }} - /> - ); - }; - +jest.mock('@elastic/eui/lib/services/accessibility', () => { return { - ...original, - CodeEditor: CodeEditorMock, + htmlIdGenerator: () => () => `generated-id`, }; }); @@ -61,6 +30,16 @@ jest.mock('@elastic/eui', () => { }} /> ), + EuiResizeObserver: ({ + onResize, + children, + }: { + onResize(data: { height: number }): void; + children(): JSX.Element; + }) => { + onResize({ height: 1000 }); + return children(); + }, }; }); @@ -78,3 +57,40 @@ jest.mock('@kbn/monaco', () => { }, }; }); + +jest.mock('../../../../kibana_react/public', () => { + const original = jest.requireActual('../../../../kibana_react/public'); + + /** + * We mock the CodeEditor because it requires the + * with the uiSettings passed down. Let's use a simple in our tests. + */ + const CodeEditorMock = (props: any) => { + // Forward our deterministic ID to the consumer + // We need below for the PainlessLang.getSyntaxErrors mock + props.editorDidMount({ + getModel() { + return { + id: EDITOR_ID, + }; + }, + }); + + return ( + ) => { + props.onChange(e.target.value); + }} + /> + ); + }; + + return { + ...original, + toMountPoint: (node: React.ReactNode) => node, + CodeEditor: CodeEditorMock, + }; +}); diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/mocks.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/mocks.ts new file mode 100644 index 0000000000000..8dfdd13e8338d --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/mocks.ts @@ -0,0 +1,25 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +interface PreviewErrorArgs { + reason: string; + scriptStack?: string[]; + position?: { offset: number; start: number; end: number } | null; +} + +export const createPreviewError = ({ + reason, + scriptStack = [], + position = null, +}: PreviewErrorArgs) => { + return { + caused_by: { reason }, + position, + script_stack: scriptStack, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/setup_environment.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/setup_environment.tsx new file mode 100644 index 0000000000000..d87b49d35c68e --- /dev/null +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/setup_environment.tsx @@ -0,0 +1,122 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './jest.mocks'; + +import React, { FunctionComponent } from 'react'; +import axios from 'axios'; +import axiosXhrAdapter from 'axios/lib/adapters/xhr'; +import { merge } from 'lodash'; + +import { notificationServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { FieldEditorProvider, Context } from '../../../public/components/field_editor_context'; +import { FieldPreviewProvider } from '../../../public/components/preview'; +import { initApi, ApiService } from '../../../public/lib'; +import { init as initHttpRequests } from './http_requests'; + +const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); +const dataStart = dataPluginMock.createStartContract(); +const { search, fieldFormats } = dataStart; + +export const spySearchQuery = jest.fn(); +export const spySearchQueryResponse = jest.fn(); +export const spyIndexPatternGetAllFields = jest.fn().mockImplementation(() => []); + +spySearchQuery.mockImplementation((params) => { + return { + toPromise: () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(undefined); + }, 2000); // simulate 2s latency for the HTTP request + }).then(() => spySearchQueryResponse()); + }, + }; +}); +search.search = spySearchQuery; + +let apiService: ApiService; + +export const setupEnvironment = () => { + // @ts-expect-error Axios does not fullfill HttpSetupn from core but enough for our tests + apiService = initApi(mockHttpClient); + const { server, httpRequestsMockHelpers } = initHttpRequests(); + + return { + server, + httpRequestsMockHelpers, + }; +}; + +// The format options available in the dropdown select for our tests. +export const fieldFormatsOptions = [{ id: 'upper', title: 'UpperCaseString' } as any]; + +export const indexPatternNameForTest = 'testIndexPattern'; + +export const WithFieldEditorDependencies = ( + Comp: FunctionComponent, + overridingDependencies?: Partial +) => (props: T) => { + // Setup mocks + (fieldFormats.getByFieldType as jest.MockedFunction< + typeof fieldFormats['getByFieldType'] + >).mockReturnValue(fieldFormatsOptions); + + (fieldFormats.getDefaultType as jest.MockedFunction< + typeof fieldFormats['getDefaultType'] + >).mockReturnValue({ id: 'testDefaultFormat', title: 'TestDefaultFormat' } as any); + + (fieldFormats.getInstance as jest.MockedFunction< + typeof fieldFormats['getInstance'] + >).mockImplementation((id: string) => { + if (id === 'upper') { + return { + convertObject: { + html(value: string = '') { + return `${value.toUpperCase()}`; + }, + }, + } as any; + } + }); + + const dependencies: Context = { + indexPattern: { + title: indexPatternNameForTest, + fields: { getAll: spyIndexPatternGetAllFields }, + } as any, + uiSettings: uiSettingsServiceMock.createStartContract(), + fieldTypeToProcess: 'runtime', + existingConcreteFields: [], + namesNotAllowed: [], + links: { + runtimePainless: 'https://elastic.co', + }, + services: { + notifications: notificationServiceMock.createStartContract(), + search, + api: apiService, + }, + fieldFormatEditors: { + getAll: () => [], + getById: () => undefined, + }, + fieldFormats, + }; + + const mergedDependencies = merge({}, dependencies, overridingDependencies); + + return ( + + + + + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/index.ts b/src/plugins/index_pattern_field_editor/common/constants.ts similarity index 80% rename from src/plugins/index_pattern_field_editor/public/test_utils/index.ts rename to src/plugins/index_pattern_field_editor/common/constants.ts index b5d943281cd79..ecd6b1ddd408b 100644 --- a/src/plugins/index_pattern_field_editor/public/test_utils/index.ts +++ b/src/plugins/index_pattern_field_editor/common/constants.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -export * from './test_utils'; - -export * from './mocks'; - -export * from './helpers'; +export const API_BASE_PATH = '/api/index_pattern_field_editor'; diff --git a/src/plugins/index_pattern_field_editor/kibana.json b/src/plugins/index_pattern_field_editor/kibana.json index 02308b349d4ca..898e7c564e57f 100644 --- a/src/plugins/index_pattern_field_editor/kibana.json +++ b/src/plugins/index_pattern_field_editor/kibana.json @@ -1,7 +1,7 @@ { "id": "indexPatternFieldEditor", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["data"], "optionalPlugins": ["usageCollection"], diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/confirm_modals/delete_field_modal.tsx similarity index 100% rename from src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx rename to src/plugins/index_pattern_field_editor/public/components/confirm_modals/delete_field_modal.tsx diff --git a/src/plugins/index_pattern_field_editor/public/components/confirm_modals/index.ts b/src/plugins/index_pattern_field_editor/public/components/confirm_modals/index.ts new file mode 100644 index 0000000000000..2283070f6f727 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/confirm_modals/index.ts @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { DeleteFieldModal } from './delete_field_modal'; + +export { ModifiedFieldModal } from './modified_field_modal'; + +export { SaveFieldTypeOrNameChangedModal } from './save_field_type_or_name_changed_modal'; diff --git a/src/plugins/index_pattern_field_editor/public/components/confirm_modals/modified_field_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/confirm_modals/modified_field_modal.tsx new file mode 100644 index 0000000000000..c9fabbaa73561 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/confirm_modals/modified_field_modal.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal } from '@elastic/eui'; + +const i18nTexts = { + title: i18n.translate('indexPatternFieldEditor.cancelField.confirmationModal.title', { + defaultMessage: 'Discard changes', + }), + description: i18n.translate('indexPatternFieldEditor.cancelField.confirmationModal.description', { + defaultMessage: `Changes that you've made to your field will be discarded, are you sure you want to continue?`, + }), + cancelButton: i18n.translate( + 'indexPatternFieldEditor.cancelField.confirmationModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ), +}; + +interface Props { + onConfirm: () => void; + onCancel: () => void; +} + +export const ModifiedFieldModal: React.FC = ({ onCancel, onConfirm }) => { + return ( + +

{i18nTexts.description}

+
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/confirm_modals/save_field_type_or_name_changed_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/confirm_modals/save_field_type_or_name_changed_modal.tsx new file mode 100644 index 0000000000000..51af86868c632 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/confirm_modals/save_field_type_or_name_changed_modal.tsx @@ -0,0 +1,86 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useState } from 'react'; +import { EuiCallOut, EuiSpacer, EuiConfirmModal, EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const geti18nTexts = (fieldName: string) => ({ + cancelButtonText: i18n.translate( + 'indexPatternFieldEditor.saveRuntimeField.confirmationModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ), + confirmButtonText: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.saveButtonLabel', + { + defaultMessage: 'Save changes', + } + ), + warningChangingFields: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningChangingFields', + { + defaultMessage: + 'Changing name or type can break searches and visualizations that rely on this field.', + } + ), + typeConfirm: i18n.translate('indexPatternFieldEditor.saveRuntimeField.confirmModal.typeConfirm', { + defaultMessage: 'Enter CHANGE to continue', + }), + titleConfirmChanges: i18n.translate( + 'indexPatternFieldEditor.saveRuntimeField.confirmModal.title', + { + defaultMessage: `Save changes to '{name}'`, + values: { + name: fieldName, + }, + } + ), +}); + +interface Props { + fieldName: string; + onConfirm: () => void; + onCancel: () => void; +} + +export const SaveFieldTypeOrNameChangedModal: React.FC = ({ + fieldName, + onCancel, + onConfirm, +}) => { + const i18nTexts = geti18nTexts(fieldName); + const [confirmContent, setConfirmContent] = useState(''); + + return ( + + + + + setConfirmContent(e.target.value)} + data-test-subj="saveModalConfirmText" + /> + + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts index 82711f707fa19..e262d3ecbfe45 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts @@ -34,4 +34,8 @@ export const RUNTIME_FIELD_OPTIONS: Array> label: 'Boolean', value: 'boolean', }, + { + label: 'Geo point', + value: 'geo_point', + }, ]; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx index 77ef0903bc6fc..b46d587dc4146 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -9,6 +9,7 @@ import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { get } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, @@ -17,20 +18,20 @@ import { EuiCode, EuiCallOut, } from '@elastic/eui'; -import type { CoreStart } from 'src/core/public'; import { Form, useForm, useFormData, + useFormIsModified, FormHook, UseField, TextField, RuntimeType, - IndexPattern, - DataPublicPluginStart, } from '../../shared_imports'; -import { Field, InternalFieldType, PluginStart } from '../../types'; +import { Field } from '../../types'; +import { useFieldEditorContext } from '../field_editor_context'; +import { useFieldPreviewContext } from '../preview'; import { RUNTIME_FIELD_OPTIONS } from './constants'; import { schema } from './form_schema'; @@ -63,36 +64,12 @@ export interface FieldFormInternal extends Omit } export interface Props { - /** Link URLs to our doc site */ - links: { - runtimePainless: string; - }; /** Optional field to edit */ field?: Field; /** Handler to receive state changes updates */ onChange?: (state: FieldEditorFormState) => void; - indexPattern: IndexPattern; - fieldFormatEditors: PluginStart['fieldFormatEditors']; - fieldFormats: DataPublicPluginStart['fieldFormats']; - uiSettings: CoreStart['uiSettings']; - /** Context object */ - ctx: { - /** The internal field type we are dealing with (concrete|runtime)*/ - fieldTypeToProcess: InternalFieldType; - /** - * An array of field names not allowed. - * e.g we probably don't want a user to give a name of an existing - * runtime field (for that the user should edit the existing runtime field). - */ - namesNotAllowed: string[]; - /** - * An array of existing concrete fields. If the user gives a name to the runtime - * field that matches one of the concrete fields, a callout will be displayed - * to indicate that this runtime field will shadow the concrete field. - * It is also used to provide the list of field autocomplete suggestions to the code editor. - */ - existingConcreteFields: Array<{ name: string; type: string }>; - }; + /** Handler to receive update on the form "isModified" state */ + onFormModifiedChange?: (isModified: boolean) => void; syntaxError: ScriptSyntaxError; } @@ -173,31 +150,53 @@ const formSerializer = (field: FieldFormInternal): Field => { }; }; -const FieldEditorComponent = ({ - field, - onChange, - links, - indexPattern, - fieldFormatEditors, - fieldFormats, - uiSettings, - syntaxError, - ctx: { fieldTypeToProcess, namesNotAllowed, existingConcreteFields }, -}: Props) => { +const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxError }: Props) => { + const { + links, + namesNotAllowed, + existingConcreteFields, + fieldTypeToProcess, + } = useFieldEditorContext(); + const { + params: { update: updatePreviewParams }, + panel: { setIsVisible: setIsPanelVisible }, + } = useFieldPreviewContext(); const { form } = useForm({ defaultValue: field, schema, deserializer: formDeserializer, serializer: formSerializer, }); - const { submit, isValid: isFormValid, isSubmitted } = form; + const { submit, isValid: isFormValid, isSubmitted, getFields } = form; const { clear: clearSyntaxError } = syntaxError; - const [{ type }] = useFormData({ form }); - const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); const i18nTexts = geti18nTexts(); + const [formData] = useFormData({ form }); + const isFormModified = useFormIsModified({ + form, + discard: [ + '__meta__.isCustomLabelVisible', + '__meta__.isValueVisible', + '__meta__.isFormatVisible', + '__meta__.isPopularityVisible', + ], + }); + + const { + name: updatedName, + type: updatedType, + script: updatedScript, + format: updatedFormat, + } = formData; + const { name: nameField, type: typeField } = getFields(); + const nameHasChanged = (Boolean(field?.name) && nameField?.isModified) ?? false; + const typeHasChanged = (Boolean(field?.type) && typeField?.isModified) ?? false; + + const isValueVisible = get(formData, '__meta__.isValueVisible'); + const isFormatVisible = get(formData, '__meta__.isFormatVisible'); + useEffect(() => { if (onChange) { onChange({ isValid: isFormValid, isSubmitted, submit }); @@ -208,18 +207,39 @@ const FieldEditorComponent = ({ // Whenever the field "type" changes we clear any possible painless syntax // error as it is possibly stale. clearSyntaxError(); - }, [type, clearSyntaxError]); + }, [updatedType, clearSyntaxError]); - const [{ name: updatedName, type: updatedType }] = useFormData({ form }); - const nameHasChanged = Boolean(field?.name) && field?.name !== updatedName; - const typeHasChanged = - Boolean(field?.type) && field?.type !== (updatedType && updatedType[0].value); + useEffect(() => { + updatePreviewParams({ + name: Boolean(updatedName?.trim()) ? updatedName : null, + type: updatedType?.[0].value, + script: + isValueVisible === false || Boolean(updatedScript?.source.trim()) === false + ? null + : updatedScript, + format: updatedFormat?.id !== undefined ? updatedFormat : null, + }); + }, [updatedName, updatedType, updatedScript, isValueVisible, updatedFormat, updatePreviewParams]); + + useEffect(() => { + if (isValueVisible || isFormatVisible) { + setIsPanelVisible(true); + } else { + setIsPanelVisible(false); + } + }, [isValueVisible, isFormatVisible, setIsPanelVisible]); + + useEffect(() => { + if (onFormModifiedChange) { + onFormModifiedChange(isFormModified); + } + }, [isFormModified, onFormModifiedChange]); return (
{/* Name */} @@ -296,12 +316,7 @@ const FieldEditorComponent = ({ data-test-subj="formatRow" withDividerRule > - + {/* Advanced settings */} @@ -320,4 +335,4 @@ const FieldEditorComponent = ({ ); }; -export const FieldEditor = React.memo(FieldEditorComponent); +export const FieldEditor = React.memo(FieldEditorComponent) as typeof FieldEditorComponent; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx index db98e4a159162..2ff4a48477def 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx @@ -9,16 +9,13 @@ import React, { useState, useEffect, useRef } from 'react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { UseField, useFormData, ES_FIELD_TYPES, useFormContext } from '../../../shared_imports'; -import { FormatSelectEditor, FormatSelectEditorProps } from '../../field_format_editor'; -import { FieldFormInternal } from '../field_editor'; -import { FieldFormatConfig } from '../../../types'; +import { useFieldEditorContext } from '../../field_editor_context'; +import { FormatSelectEditor } from '../../field_format_editor'; +import type { FieldFormInternal } from '../field_editor'; +import type { FieldFormatConfig } from '../../../types'; -export const FormatField = ({ - indexPattern, - fieldFormatEditors, - fieldFormats, - uiSettings, -}: Omit) => { +export const FormatField = () => { + const { indexPattern, uiSettings, fieldFormats, fieldFormatEditors } = useFieldEditorContext(); const isMounted = useRef(false); const [{ type }] = useFormData({ watch: ['name', 'type'] }); const { getFields, isSubmitted } = useFormContext(); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx index 29945e15874b7..d73e8046e5db7 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -139,6 +139,7 @@ export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxEr <> { defaultMessage: 'Type select', } )} + aria-controls="runtimeFieldScript" fullWidth /> diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts index 2d324804c9e43..ba44682ba65e0 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { ValidationFunc, FieldConfig } from '../../shared_imports'; import { Field } from '../../types'; import { schema } from './form_schema'; -import { Props } from './field_editor'; +import type { Props } from './field_editor'; const createNameNotAllowedValidator = ( namesNotAllowed: string[] diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_context.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_context.tsx new file mode 100644 index 0000000000000..74bf2657ba3de --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_context.tsx @@ -0,0 +1,96 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { createContext, useContext, FunctionComponent, useMemo } from 'react'; +import { NotificationsStart, CoreStart } from 'src/core/public'; +import type { IndexPattern, DataPublicPluginStart } from '../shared_imports'; +import { ApiService } from '../lib/api'; +import type { InternalFieldType, PluginStart } from '../types'; + +export interface Context { + indexPattern: IndexPattern; + fieldTypeToProcess: InternalFieldType; + uiSettings: CoreStart['uiSettings']; + links: { + runtimePainless: string; + }; + services: { + search: DataPublicPluginStart['search']; + api: ApiService; + notifications: NotificationsStart; + }; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + /** + * An array of field names not allowed. + * e.g we probably don't want a user to give a name of an existing + * runtime field (for that the user should edit the existing runtime field). + */ + namesNotAllowed: string[]; + /** + * An array of existing concrete fields. If the user gives a name to the runtime + * field that matches one of the concrete fields, a callout will be displayed + * to indicate that this runtime field will shadow the concrete field. + * It is also used to provide the list of field autocomplete suggestions to the code editor. + */ + existingConcreteFields: Array<{ name: string; type: string }>; +} + +const fieldEditorContext = createContext(undefined); + +export const FieldEditorProvider: FunctionComponent = ({ + services, + indexPattern, + links, + uiSettings, + fieldTypeToProcess, + fieldFormats, + fieldFormatEditors, + namesNotAllowed, + existingConcreteFields, + children, +}) => { + const ctx = useMemo( + () => ({ + indexPattern, + fieldTypeToProcess, + links, + uiSettings, + services, + fieldFormats, + fieldFormatEditors, + namesNotAllowed, + existingConcreteFields, + }), + [ + indexPattern, + fieldTypeToProcess, + services, + links, + uiSettings, + fieldFormats, + fieldFormatEditors, + namesNotAllowed, + existingConcreteFields, + ] + ); + + return {children}; +}; + +export const useFieldEditorContext = (): Context => { + const ctx = useContext(fieldEditorContext); + + if (ctx === undefined) { + throw new Error('useFieldEditorContext must be used within a '); + } + + return ctx; +}; + +export const useFieldEditorServices = () => useFieldEditorContext().services; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx index 13830f9233b5e..19015aa9d0d10 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -6,13 +6,10 @@ * Side Public License, v 1. */ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlyoutHeader, - EuiFlyoutBody, - EuiFlyoutFooter, EuiTitle, EuiFlexGroup, EuiFlexItem, @@ -21,64 +18,32 @@ import { EuiCallOut, EuiSpacer, EuiText, - EuiConfirmModal, - EuiFieldText, - EuiFormRow, } from '@elastic/eui'; -import { DocLinksStart, CoreStart } from 'src/core/public'; +import type { Field, EsRuntimeField } from '../types'; +import { RuntimeFieldPainlessError } from '../lib'; +import { euiFlyoutClassname } from '../constants'; +import { FlyoutPanels } from './flyout_panels'; +import { useFieldEditorContext } from './field_editor_context'; +import { FieldEditor, FieldEditorFormState } from './field_editor/field_editor'; +import { FieldPreview, useFieldPreviewContext } from './preview'; +import { ModifiedFieldModal, SaveFieldTypeOrNameChangedModal } from './confirm_modals'; -import { Field, InternalFieldType, PluginStart, EsRuntimeField } from '../types'; -import { getLinks, RuntimeFieldPainlessError } from '../lib'; -import type { IndexPattern, DataPublicPluginStart } from '../shared_imports'; -import type { Props as FieldEditorProps, FieldEditorFormState } from './field_editor/field_editor'; +const i18nTexts = { + cancelButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCancelButtonLabel', { + defaultMessage: 'Cancel', + }), + saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { + defaultMessage: 'Save', + }), + formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', { + defaultMessage: 'Fix errors in form before continuing.', + }), +}; -const geti18nTexts = (field?: Field) => { - return { - closeButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCloseButtonLabel', { - defaultMessage: 'Close', - }), - saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { - defaultMessage: 'Save', - }), - formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', { - defaultMessage: 'Fix errors in form before continuing.', - }), - cancelButtonText: i18n.translate( - 'indexPatternFieldEditor.saveRuntimeField.confirmationModal.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } - ), - confirmButtonText: i18n.translate( - 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.saveButtonLabel', - { - defaultMessage: 'Save changes', - } - ), - warningChangingFields: i18n.translate( - 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningChangingFields', - { - defaultMessage: - 'Changing name or type can break searches and visualizations that rely on this field.', - } - ), - typeConfirm: i18n.translate( - 'indexPatternFieldEditor.saveRuntimeField.confirmModal.typeConfirm', - { - defaultMessage: 'Enter CHANGE to continue', - } - ), - titleConfirmChanges: i18n.translate( - 'indexPatternFieldEditor.saveRuntimeField.confirmModal.title', - { - defaultMessage: `Save changes to '{name}'`, - values: { - name: field?.name, - }, - } - ), - }; +const defaultModalVisibility = { + confirmChangeNameOrType: false, + confirmUnsavedChanges: false, }; export interface Props { @@ -90,44 +55,30 @@ export interface Props { * Handler for the "cancel" footer button */ onCancel: () => void; - /** - * The docLinks start service from core - */ - docLinks: DocLinksStart; - /** - * The Field editor component that contains the form to create or edit a field - */ - FieldEditor: React.ComponentType | null; - /** The internal field type we are dealing with (concrete|runtime)*/ - fieldTypeToProcess: InternalFieldType; /** Handler to validate the script */ runtimeFieldValidator: (field: EsRuntimeField) => Promise; /** Optional field to process */ field?: Field; - - indexPattern: IndexPattern; - fieldFormatEditors: PluginStart['fieldFormatEditors']; - fieldFormats: DataPublicPluginStart['fieldFormats']; - uiSettings: CoreStart['uiSettings']; isSavingField: boolean; + /** Handler to call when the component mounts. + * We will pass "up" data that the parent component might need + */ + onMounted?: (args: { canCloseValidator: () => boolean }) => void; } const FieldEditorFlyoutContentComponent = ({ field, onSave, onCancel, - FieldEditor, - docLinks, - indexPattern, - fieldFormatEditors, - fieldFormats, - uiSettings, - fieldTypeToProcess, runtimeFieldValidator, isSavingField, + onMounted, }: Props) => { const isEditingExistingField = !!field; - const i18nTexts = geti18nTexts(field); + const { indexPattern } = useFieldEditorContext(); + const { + panel: { isVisible: isPanelVisible }, + } = useFieldPreviewContext(); const [formState, setFormState] = useState({ isSubmitted: false, @@ -142,12 +93,11 @@ const FieldEditorFlyoutContentComponent = ({ ); const [isValidating, setIsValidating] = useState(false); - const [isModalVisible, setIsModalVisible] = useState(false); - const [confirmContent, setConfirmContent] = useState(''); + const [modalVisibility, setModalVisibility] = useState(defaultModalVisibility); + const [isFormModified, setIsFormModified] = useState(false); const { submit, isValid: isFormValid, isSubmitted } = formState; - const { fields } = indexPattern; - const isSaveButtonDisabled = isFormValid === false || painlessSyntaxError !== null; + const hasErrors = isFormValid === false || painlessSyntaxError !== null; const clearSyntaxError = useCallback(() => setPainlessSyntaxError(null), []); @@ -159,6 +109,16 @@ const FieldEditorFlyoutContentComponent = ({ [painlessSyntaxError, clearSyntaxError] ); + const canCloseValidator = useCallback(() => { + if (isFormModified) { + setModalVisibility({ + ...defaultModalVisibility, + confirmUnsavedChanges: true, + }); + } + return !isFormModified; + }, [isFormModified]); + const onClickSave = useCallback(async () => { const { isValid, data } = await submit(); const nameChange = field?.name !== data.name; @@ -182,167 +142,177 @@ const FieldEditorFlyoutContentComponent = ({ } if (isEditingExistingField && (nameChange || typeChange)) { - setIsModalVisible(true); + setModalVisibility({ + ...defaultModalVisibility, + confirmChangeNameOrType: true, + }); } else { onSave(data); } } }, [onSave, submit, runtimeFieldValidator, field, isEditingExistingField]); - const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]); + const onClickCancel = useCallback(() => { + const canClose = canCloseValidator(); + + if (canClose) { + onCancel(); + } + }, [onCancel, canCloseValidator]); - const existingConcreteFields = useMemo(() => { - const existing: Array<{ name: string; type: string }> = []; + const renderModal = () => { + if (modalVisibility.confirmChangeNameOrType) { + return ( + { + const { data } = await submit(); + onSave(data); + }} + onCancel={() => { + setModalVisibility(defaultModalVisibility); + }} + /> + ); + } - fields - .filter((fld) => { - const isFieldBeingEdited = field?.name === fld.name; - return !isFieldBeingEdited && fld.isMapped; - }) - .forEach((fld) => { - existing.push({ - name: fld.name, - type: (fld.esTypes && fld.esTypes[0]) || '', - }); - }); + if (modalVisibility.confirmUnsavedChanges) { + return ( + { + setModalVisibility(defaultModalVisibility); + onCancel(); + }} + onCancel={() => { + setModalVisibility(defaultModalVisibility); + }} + /> + ); + } - return existing; - }, [fields, field]); + return null; + }; - const ctx = useMemo( - () => ({ - fieldTypeToProcess, - namesNotAllowed, - existingConcreteFields, - }), - [fieldTypeToProcess, namesNotAllowed, existingConcreteFields] - ); + useEffect(() => { + if (onMounted) { + // When the flyout mounts we send to the parent the validator to check + // if we can close the flyout or not (and display a confirm modal if needed). + // This is required to display the confirm modal when clicking outside the flyout. + onMounted({ canCloseValidator }); + + return () => { + onMounted({ canCloseValidator: () => true }); + }; + } + }, [onMounted, canCloseValidator]); - const modal = isModalVisible ? ( - { - setIsModalVisible(false); - setConfirmContent(''); - }} - onConfirm={async () => { - const { data } = await submit(); - onSave(data); - }} - > - - - - setConfirmContent(e.target.value)} - data-test-subj="saveModalConfirmText" - /> - - - ) : null; return ( <> - - -

- {field ? ( - - ) : ( - - )} -

-
- -

- {indexPattern.title}, - }} + + {/* Editor panel */} + + + + +

+ {field ? ( + + ) : ( + + )} +

+ + +

+ {indexPattern.title}, + }} + /> +

+ + + + -

- - - - - {FieldEditor && ( - - )} - - - - {FieldEditor && ( - <> - {isSubmitted && isSaveButtonDisabled && ( - <> - - - - )} - - - - {i18nTexts.closeButtonLabel} - - - - - - {i18nTexts.saveButtonLabel} - - - - + + + + <> + {isSubmitted && hasErrors && ( + <> + + + + )} + + + + {i18nTexts.cancelButtonLabel} + + + + + + {i18nTexts.saveButtonLabel} + + + + + + + + {/* Preview panel */} + {isPanelVisible && ( + + + )} - - {modal} + + + {renderModal()} ); }; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx index e01b3f9bb422c..cf2b29bbc97e8 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import React, { useCallback, useState, useMemo } from 'react'; import { DocLinksStart, NotificationsStart, CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; @@ -18,53 +18,40 @@ import { RuntimeType, UsageCollectionStart, } from '../shared_imports'; -import { Field, PluginStart, InternalFieldType } from '../types'; +import type { Field, PluginStart, InternalFieldType } from '../types'; import { pluginName } from '../constants'; -import { deserializeField, getRuntimeFieldValidator } from '../lib'; -import { Props as FieldEditorProps } from './field_editor/field_editor'; -import { FieldEditorFlyoutContent } from './field_editor_flyout_content'; - -export interface FieldEditorContext { - indexPattern: IndexPattern; - /** - * The Kibana field type of the field to create or edit - * Default: "runtime" - */ - fieldTypeToProcess: InternalFieldType; - /** The search service from the data plugin */ - search: DataPublicPluginStart['search']; -} +import { deserializeField, getRuntimeFieldValidator, getLinks, ApiService } from '../lib'; +import { + FieldEditorFlyoutContent, + Props as FieldEditorFlyoutContentProps, +} from './field_editor_flyout_content'; +import { FieldEditorProvider } from './field_editor_context'; +import { FieldPreviewProvider } from './preview'; export interface Props { - /** - * Handler for the "save" footer button - */ + /** Handler for the "save" footer button */ onSave: (field: IndexPatternField) => void; - /** - * Handler for the "cancel" footer button - */ + /** Handler for the "cancel" footer button */ onCancel: () => void; - /** - * The docLinks start service from core - */ + onMounted?: FieldEditorFlyoutContentProps['onMounted']; + /** The docLinks start service from core */ docLinks: DocLinksStart; - /** - * The context object specific to where the editor is currently being consumed - */ - ctx: FieldEditorContext; - /** - * Optional field to edit - */ + /** The index pattern where the field will be added */ + indexPattern: IndexPattern; + /** The Kibana field type of the field to create or edit (default: "runtime") */ + fieldTypeToProcess: InternalFieldType; + /** Optional field to edit */ field?: IndexPatternField; - /** - * Services - */ + /** Services */ indexPatternService: DataPublicPluginStart['indexPatterns']; notifications: NotificationsStart; + search: DataPublicPluginStart['search']; + usageCollection: UsageCollectionStart; + apiService: ApiService; + /** Field format */ fieldFormatEditors: PluginStart['fieldFormatEditors']; fieldFormats: DataPublicPluginStart['fieldFormats']; uiSettings: CoreStart['uiSettings']; - usageCollection: UsageCollectionStart; } /** @@ -78,19 +65,58 @@ export const FieldEditorFlyoutContentContainer = ({ field, onSave, onCancel, + onMounted, docLinks, + fieldTypeToProcess, + indexPattern, indexPatternService, - ctx: { indexPattern, fieldTypeToProcess, search }, + search, notifications, + usageCollection, + apiService, fieldFormatEditors, fieldFormats, uiSettings, - usageCollection, }: Props) => { const fieldToEdit = deserializeField(indexPattern, field); - const [Editor, setEditor] = useState | null>(null); const [isSaving, setIsSaving] = useState(false); + const { fields } = indexPattern; + + const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]); + + const existingConcreteFields = useMemo(() => { + const existing: Array<{ name: string; type: string }> = []; + + fields + .filter((fld) => { + const isFieldBeingEdited = field?.name === fld.name; + return !isFieldBeingEdited && fld.isMapped; + }) + .forEach((fld) => { + existing.push({ + name: fld.name, + type: (fld.esTypes && fld.esTypes[0]) || '', + }); + }); + + return existing; + }, [fields, field]); + + const validateRuntimeField = useMemo(() => getRuntimeFieldValidator(indexPattern.title, search), [ + search, + indexPattern, + ]); + + const services = useMemo( + () => ({ + api: apiService, + search, + notifications, + }), + [apiService, search, notifications] + ); + const saveField = useCallback( async (updatedField: Field) => { setIsSaving(true); @@ -163,36 +189,28 @@ export const FieldEditorFlyoutContentContainer = ({ ] ); - const validateRuntimeField = useMemo(() => getRuntimeFieldValidator(indexPattern.title, search), [ - search, - indexPattern, - ]); - - const loadEditor = useCallback(async () => { - const { FieldEditor } = await import('./field_editor'); - - setEditor(() => FieldEditor); - }, []); - - useEffect(() => { - // On mount: load the editor asynchronously - loadEditor(); - }, [loadEditor]); - return ( - + services={services} + fieldFormatEditors={fieldFormatEditors} + fieldFormats={fieldFormats} + namesNotAllowed={namesNotAllowed} + existingConcreteFields={existingConcreteFields} + > + + + + ); }; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_loader.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_loader.tsx new file mode 100644 index 0000000000000..f77db7e407caa --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_loader.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useCallback, useEffect } from 'react'; +import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; + +import type { Props } from './field_editor_flyout_content_container'; + +export const FieldEditorLoader: React.FC = (props) => { + const [Editor, setEditor] = useState | null>(null); + + const loadEditor = useCallback(async () => { + const { FieldEditorFlyoutContentContainer } = await import( + './field_editor_flyout_content_container' + ); + setEditor(() => FieldEditorFlyoutContentContainer); + }, []); + + useEffect(() => { + // On mount: load the editor asynchronously + loadEditor(); + }, [loadEditor]); + + return Editor ? ( + + ) : ( + <> + + + + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx index 129049e1b0565..fcf73f397b3fe 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx @@ -10,8 +10,8 @@ import React, { PureComponent, ReactText } from 'react'; import { i18n } from '@kbn/i18n'; import type { FieldFormatsContentType } from 'src/plugins/field_formats/common'; -import { Sample, SampleInput } from '../../types'; -import { FormatEditorProps } from '../types'; +import type { Sample, SampleInput } from '../../types'; +import type { FormatEditorProps } from '../types'; import { formatId } from './constants'; export const convertSampleInput = ( diff --git a/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panel.tsx b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panel.tsx new file mode 100644 index 0000000000000..05f127c09c996 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panel.tsx @@ -0,0 +1,144 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { + CSSProperties, + useState, + useLayoutEffect, + useCallback, + createContext, + useContext, + useMemo, +} from 'react'; +import classnames from 'classnames'; +import { EuiFlexItem } from '@elastic/eui'; + +import { useFlyoutPanelsContext } from './flyout_panels'; + +interface Context { + registerFooter: () => void; + registerContent: () => void; +} + +const flyoutPanelContext = createContext({ + registerFooter: () => {}, + registerContent: () => {}, +}); + +export interface Props { + /** Width of the panel (in percent % or in px if the "fixedPanelWidths" prop is set to true on the panels group) */ + width?: number; + /** EUI sass background */ + backgroundColor?: 'euiPageBackground' | 'euiEmptyShade'; + /** Add a border to the panel */ + border?: 'left' | 'right'; + 'data-test-subj'?: string; +} + +export const Panel: React.FC> = ({ + children, + width, + className = '', + backgroundColor, + border, + 'data-test-subj': dataTestSubj, + ...rest +}) => { + const [config, setConfig] = useState<{ hasFooter: boolean; hasContent: boolean }>({ + hasContent: false, + hasFooter: false, + }); + + const [styles, setStyles] = useState({}); + + /* eslint-disable @typescript-eslint/naming-convention */ + const classes = classnames('fieldEditor__flyoutPanel', className, { + 'fieldEditor__flyoutPanel--pageBackground': backgroundColor === 'euiPageBackground', + 'fieldEditor__flyoutPanel--emptyShade': backgroundColor === 'euiEmptyShade', + 'fieldEditor__flyoutPanel--leftBorder': border === 'left', + 'fieldEditor__flyoutPanel--rightBorder': border === 'right', + 'fieldEditor__flyoutPanel--withContent': config.hasContent, + }); + /* eslint-enable @typescript-eslint/naming-convention */ + + const { addPanel } = useFlyoutPanelsContext(); + + const registerContent = useCallback(() => { + setConfig((prev) => { + return { + ...prev, + hasContent: true, + }; + }); + }, []); + + const registerFooter = useCallback(() => { + setConfig((prev) => { + if (!prev.hasContent) { + throw new Error( + 'You need to add a when you add a ' + ); + } + return { + ...prev, + hasFooter: true, + }; + }); + }, []); + + const ctx = useMemo(() => ({ registerContent, registerFooter }), [ + registerFooter, + registerContent, + ]); + + useLayoutEffect(() => { + const { removePanel, isFixedWidth } = addPanel({ width }); + + if (width) { + setStyles((prev) => { + if (isFixedWidth) { + return { + ...prev, + width: `${width}px`, + }; + } + return { + ...prev, + minWidth: `${width}%`, + }; + }); + } + + return removePanel; + }, [width, addPanel]); + + return ( + + +
+ {children} +
+
+
+ ); +}; + +export const useFlyoutPanelContext = (): Context => { + const ctx = useContext(flyoutPanelContext); + + if (ctx === undefined) { + throw new Error('useFlyoutPanel() must be used within a '); + } + + return ctx; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels.scss b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels.scss new file mode 100644 index 0000000000000..29a62a16db213 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels.scss @@ -0,0 +1,48 @@ +.fieldEditor__flyoutPanels { + height: 100%; + + &__column { + height: 100%; + overflow: hidden; + } +} + +.fieldEditor__flyoutPanel { + height: 100%; + overflow-y: auto; + padding: $euiSizeL; + + &--pageBackground { + background-color: $euiPageBackgroundColor; + } + &--emptyShade { + background-color: $euiColorEmptyShade; + } + &--leftBorder { + border-left: $euiBorderThin; + } + &--rightBorder { + border-right: $euiBorderThin; + } + &--withContent { + padding: 0; + overflow-y: hidden; + display: flex; + flex-direction: column; + } + + &__header { + padding: 0 !important; + } + + &__content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: $euiSizeL; + } + + &__footer { + flex: 0; + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels.tsx b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels.tsx new file mode 100644 index 0000000000000..95fb44b293e00 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels.tsx @@ -0,0 +1,145 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { + useState, + createContext, + useContext, + useCallback, + useMemo, + useLayoutEffect, +} from 'react'; +import { EuiFlexGroup, EuiFlexGroupProps } from '@elastic/eui'; + +import './flyout_panels.scss'; + +interface Panel { + width?: number; +} + +interface Context { + addPanel: (panel: Panel) => { removePanel: () => void; isFixedWidth: boolean }; +} + +let idx = 0; + +const panelId = () => idx++; + +const flyoutPanelsContext = createContext({ + addPanel() { + return { + removePanel: () => {}, + isFixedWidth: false, + }; + }, +}); + +const limitWidthToWindow = (width: number, { innerWidth }: Window): number => + Math.min(width, innerWidth * 0.8); + +export interface Props { + /** + * The total max width with all the panels in the DOM + * Corresponds to the "maxWidth" prop passed to the EuiFlyout + */ + maxWidth: number; + /** The className selector of the flyout */ + flyoutClassName: string; + /** The size between the panels. Corresponds to EuiFlexGroup gutterSize */ + gutterSize?: EuiFlexGroupProps['gutterSize']; + /** Flag to indicate if the panels width are declared as fixed pixel width instead of percent */ + fixedPanelWidths?: boolean; +} + +export const Panels: React.FC = ({ + maxWidth, + flyoutClassName, + fixedPanelWidths = false, + ...props +}) => { + const flyoutDOMelement = useMemo(() => { + const el = document.getElementsByClassName(flyoutClassName); + + if (el.length === 0) { + return null; + } + + return el.item(0) as HTMLDivElement; + }, [flyoutClassName]); + + const [panels, setPanels] = useState<{ [id: number]: Panel }>({}); + + const removePanel = useCallback((id: number) => { + setPanels((prev) => { + const { [id]: panelToRemove, ...rest } = prev; + return rest; + }); + }, []); + + const addPanel = useCallback( + (panel: Panel) => { + const nextId = panelId(); + setPanels((prev) => { + return { ...prev, [nextId]: panel }; + }); + + return { + removePanel: removePanel.bind(null, nextId), + isFixedWidth: fixedPanelWidths, + }; + }, + [removePanel, fixedPanelWidths] + ); + + const ctx: Context = useMemo( + () => ({ + addPanel, + }), + [addPanel] + ); + + useLayoutEffect(() => { + if (!flyoutDOMelement) { + return; + } + + let currentWidth: number; + + if (fixedPanelWidths) { + const totalWidth = Object.values(panels).reduce((acc, { width = 0 }) => acc + width, 0); + currentWidth = Math.min(maxWidth, totalWidth); + // As EUI declares both min-width and max-width on the .euiFlyout CSS class + // we need to override both values + flyoutDOMelement.style.minWidth = `${limitWidthToWindow(currentWidth, window)}px`; + flyoutDOMelement.style.maxWidth = `${limitWidthToWindow(currentWidth, window)}px`; + } else { + const totalPercentWidth = Math.min( + 100, + Object.values(panels).reduce((acc, { width = 0 }) => acc + width, 0) + ); + currentWidth = (maxWidth * totalPercentWidth) / 100; + flyoutDOMelement.style.maxWidth = `${limitWidthToWindow(currentWidth, window)}px`; + } + }, [panels, maxWidth, fixedPanelWidths, flyoutClassName, flyoutDOMelement]); + + return ( + + + + ); +}; + +export const useFlyoutPanelsContext = (): Context => { + const ctx = useContext(flyoutPanelsContext); + + if (ctx === undefined) { + throw new Error(' must be used within a wrapper'); + } + + return ctx; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_content.tsx b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_content.tsx new file mode 100644 index 0000000000000..ef2f7498a4c22 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_content.tsx @@ -0,0 +1,21 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useEffect } from 'react'; + +import { useFlyoutPanelContext } from './flyout_panel'; + +export const PanelContent: React.FC = (props) => { + const { registerContent } = useFlyoutPanelContext(); + + useEffect(() => { + registerContent(); + }, [registerContent]); + + // Adding a tabIndex prop to the div as it is the body of the flyout which is scrollable. + return
; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_footer.tsx b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_footer.tsx new file mode 100644 index 0000000000000..8a987420dd84b --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_footer.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useEffect } from 'react'; +import { EuiFlyoutFooter, EuiFlyoutFooterProps } from '@elastic/eui'; + +import { useFlyoutPanelContext } from './flyout_panel'; + +export const PanelFooter: React.FC< + { children: React.ReactNode } & Omit +> = (props) => { + const { registerFooter } = useFlyoutPanelContext(); + + useEffect(() => { + registerFooter(); + }, [registerFooter]); + + return ; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_header.tsx b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_header.tsx new file mode 100644 index 0000000000000..00edf1c637fc1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/flyout_panels_header.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiSpacer, EuiFlyoutHeader, EuiFlyoutHeaderProps } from '@elastic/eui'; + +export const PanelHeader: React.FunctionComponent< + { children: React.ReactNode } & Omit +> = (props) => ( + <> + + + +); diff --git a/src/plugins/index_pattern_field_editor/public/components/flyout_panels/index.ts b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/index.ts new file mode 100644 index 0000000000000..0380a0bfefe72 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/flyout_panels/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PanelFooter } from './flyout_panels_footer'; +import { PanelHeader } from './flyout_panels_header'; +import { PanelContent } from './flyout_panels_content'; +import { Panel } from './flyout_panel'; +import { Panels } from './flyout_panels'; + +export { useFlyoutPanelContext } from './flyout_panel'; + +export const FlyoutPanels = { + Group: Panels, + Item: Panel, + Content: PanelContent, + Header: PanelHeader, + Footer: PanelFooter, +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/index.ts b/src/plugins/index_pattern_field_editor/public/components/index.ts index 9f7f40fcadec7..927e28a8e3adf 100644 --- a/src/plugins/index_pattern_field_editor/public/components/index.ts +++ b/src/plugins/index_pattern_field_editor/public/components/index.ts @@ -6,17 +6,6 @@ * Side Public License, v 1. */ -export { - FieldEditorFlyoutContent, - Props as FieldEditorFlyoutContentProps, -} from './field_editor_flyout_content'; - -export { - FieldEditorFlyoutContentContainer, - Props as FieldEditorFlyoutContentContainerProps, - FieldEditorContext, -} from './field_editor_flyout_content_container'; - export { getDeleteFieldProvider, Props as DeleteFieldProviderProps } from './delete_field_provider'; export * from './field_format_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/documents_nav_preview.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/documents_nav_preview.tsx new file mode 100644 index 0000000000000..fa4097725cde1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/documents_nav_preview.tsx @@ -0,0 +1,132 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiButtonIcon, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { useFieldPreviewContext } from './field_preview_context'; + +export const DocumentsNavPreview = () => { + const { + currentDocument: { id: documentId, isCustomId }, + documents: { loadSingle, loadFromCluster }, + navigation: { prev, next }, + error, + } = useFieldPreviewContext(); + + const errorMessage = + error !== null && error.code === 'DOC_NOT_FOUND' + ? i18n.translate( + 'indexPatternFieldEditor.fieldPreview.documentIdField.documentNotFoundError', + { + defaultMessage: 'Document not found', + } + ) + : null; + + const isInvalid = error !== null && error.code === 'DOC_NOT_FOUND'; + + // We don't display the nav button when the user has entered a custom + // document ID as at that point there is no more reference to what's "next" + const showNavButtons = isCustomId === false; + + const onDocumentIdChange = useCallback( + (e: React.SyntheticEvent) => { + const nextId = (e.target as HTMLInputElement).value; + loadSingle(nextId); + }, + [loadSingle] + ); + + return ( +
+ + + + + + {isCustomId && ( + + loadFromCluster()} + data-test-subj="loadDocsFromClusterButton" + > + {i18n.translate( + 'indexPatternFieldEditor.fieldPreview.documentIdField.loadDocumentsFromCluster', + { + defaultMessage: 'Load documents from cluster', + } + )} + + + )} + + + {showNavButtons && ( + + + + + + + + + + + )} + +
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list.scss b/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list.scss new file mode 100644 index 0000000000000..d1bb8cb5731c9 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list.scss @@ -0,0 +1,63 @@ +/** +[1] This corresponds to the ITEM_HEIGHT declared in "field_list.tsx" +[2] This corresponds to the SHOW_MORE_HEIGHT declared in "field_list.tsx" +[3] We need the tooltip to be 100% to display the text ellipsis of the field value +*/ + +$previewFieldItemHeight: 40px; /* [1] */ +$previewShowMoreHeight: 40px; /* [2] */ + +.indexPatternFieldEditor__previewFieldList { + position: relative; + + &__item { + border-bottom: $euiBorderThin; + height: $previewFieldItemHeight; + align-items: center; + overflow: hidden; + + &--highlighted { + $backgroundColor: tintOrShade($euiColorWarning, 90%, 70%); + background: $backgroundColor; + font-weight: 600; + } + + &__key, &__value { + overflow: hidden; + } + + &__actions { + flex-basis: 24px !important; + } + + &__actionsBtn { + display: none; + } + + &--pinned .indexPatternFieldEditor__previewFieldList__item__actionsBtn, + &:hover .indexPatternFieldEditor__previewFieldList__item__actionsBtn { + display: block; + } + + &__value .euiToolTipAnchor { + width: 100%; /* [3] */ + } + + &__key__wrapper, &__value__wrapper { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + width: 100%; + } + } + + &__showMore { + position: absolute; + width: 100%; + height: $previewShowMoreHeight; + bottom: $previewShowMoreHeight * -1; + display: flex; + align-items: flex-end; + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list.tsx new file mode 100644 index 0000000000000..aae0f0c74a5f1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list.tsx @@ -0,0 +1,235 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useState, useMemo, useCallback } from 'react'; +import VirtualList from 'react-tiny-virtual-list'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; +import { EuiButtonEmpty, EuiButton, EuiSpacer, EuiEmptyPrompt, EuiTextColor } from '@elastic/eui'; + +import { useFieldEditorContext } from '../../field_editor_context'; +import { + useFieldPreviewContext, + defaultValueFormatter, + FieldPreview, +} from '../field_preview_context'; +import { PreviewListItem } from './field_list_item'; + +import './field_list.scss'; + +const ITEM_HEIGHT = 40; +const SHOW_MORE_HEIGHT = 40; +const INITIAL_MAX_NUMBER_OF_FIELDS = 7; + +export type DocumentField = FieldPreview & { + isPinned?: boolean; +}; + +interface Props { + height: number; + clearSearch: () => void; + searchValue?: string; +} + +/** + * Escape regex special characters (e.g /, ^, $...) with a "\" + * Copied from https://stackoverflow.com/a/9310752 + */ +function escapeRegExp(text: string) { + return text.replace(/[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); +} + +function fuzzyMatch(searchValue: string, text: string) { + const pattern = `.*${searchValue.split('').map(escapeRegExp).join('.*')}.*`; + const regex = new RegExp(pattern, 'i'); + return regex.test(text); +} + +export const PreviewFieldList: React.FC = ({ height, clearSearch, searchValue = '' }) => { + const { indexPattern } = useFieldEditorContext(); + const { + currentDocument: { value: currentDocument }, + pinnedFields: { value: pinnedFields, set: setPinnedFields }, + } = useFieldPreviewContext(); + + const [showAllFields, setShowAllFields] = useState(false); + + const { + fields: { getAll: getAllFields }, + } = indexPattern; + + const indexPatternFields = useMemo(() => { + return getAllFields(); + }, [getAllFields]); + + const fieldList: DocumentField[] = useMemo( + () => + indexPatternFields + .map(({ name, displayName }) => { + const value = get(currentDocument?._source, name); + const formattedValue = defaultValueFormatter(value); + + return { + key: displayName, + value, + formattedValue, + isPinned: false, + }; + }) + .filter(({ value }) => value !== undefined), + [indexPatternFields, currentDocument?._source] + ); + + const fieldListWithPinnedFields: DocumentField[] = useMemo(() => { + const pinned: DocumentField[] = []; + const notPinned: DocumentField[] = []; + + fieldList.forEach((field) => { + if (pinnedFields[field.key]) { + pinned.push({ ...field, isPinned: true }); + } else { + notPinned.push({ ...field, isPinned: false }); + } + }); + + return [...pinned, ...notPinned]; + }, [fieldList, pinnedFields]); + + const { filteredFields, totalFields } = useMemo(() => { + const list = + searchValue.trim() === '' + ? fieldListWithPinnedFields + : fieldListWithPinnedFields.filter(({ key }) => fuzzyMatch(searchValue, key)); + + const total = list.length; + + if (showAllFields) { + return { + filteredFields: list, + totalFields: total, + }; + } + + return { + filteredFields: list.filter((_, i) => i < INITIAL_MAX_NUMBER_OF_FIELDS), + totalFields: total, + }; + }, [fieldListWithPinnedFields, showAllFields, searchValue]); + + const hasSearchValue = searchValue.trim() !== ''; + const isEmptySearchResultVisible = hasSearchValue && totalFields === 0; + + // "height" corresponds to the total height of the flex item that occupies the remaining + // vertical space up to the bottom of the flyout panel. We don't want to give that height + // to the virtual list because it would mean that the "Show more" button would be pinned to the + // bottom of the panel all the time. Which is not what we want when we render initially a few + // fields. + const listHeight = Math.min(filteredFields.length * ITEM_HEIGHT, height - SHOW_MORE_HEIGHT); + + const toggleShowAllFields = useCallback(() => { + setShowAllFields((prev) => !prev); + }, []); + + const toggleIsPinnedField = useCallback( + (name) => { + setPinnedFields((prev) => { + const isPinned = !prev[name]; + return { + ...prev, + [name]: isPinned, + }; + }); + }, + [setPinnedFields] + ); + + const renderEmptyResult = () => { + return ( + <> + + +

+ {i18n.translate( + 'indexPatternFieldEditor.fieldPreview.searchResult.emptyPromptTitle', + { + defaultMessage: 'No matching fields in this index pattern', + } + )} +

+ + } + titleSize="xs" + actions={ + + {i18n.translate( + 'indexPatternFieldEditor.fieldPreview.searchResult.emptyPrompt.clearSearchButtonLabel', + { + defaultMessage: 'Clear search', + } + )} + + } + data-test-subj="emptySearchResult" + /> + + ); + }; + + const renderToggleFieldsButton = () => + totalFields <= INITIAL_MAX_NUMBER_OF_FIELDS ? null : ( +
+ + {showAllFields + ? i18n.translate('indexPatternFieldEditor.fieldPreview.showLessFieldsButtonLabel', { + defaultMessage: 'Show less', + }) + : i18n.translate('indexPatternFieldEditor.fieldPreview.showMoreFieldsButtonLabel', { + defaultMessage: 'Show more', + })} + +
+ ); + + if (currentDocument === undefined || height === -1) { + return null; + } + + return ( +
+ {isEmptySearchResultVisible ? ( + renderEmptyResult() + ) : ( + { + const field = filteredFields[index]; + + return ( +
+ +
+ ); + }} + /> + )} + + {renderToggleFieldsButton()} +
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list_item.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list_item.tsx new file mode 100644 index 0000000000000..348c442a1cd37 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_list/field_list_item.tsx @@ -0,0 +1,125 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import classnames from 'classnames'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiButtonIcon, EuiButtonEmpty } from '@elastic/eui'; + +import { ImagePreviewModal } from '../image_preview_modal'; +import type { DocumentField } from './field_list'; + +interface Props { + field: DocumentField; + toggleIsPinned?: (name: string) => void; + highlighted?: boolean; +} + +export const PreviewListItem: React.FC = ({ + field: { key, value, formattedValue, isPinned = false }, + highlighted, + toggleIsPinned, +}) => { + const [isPreviewImageModalVisible, setIsPreviewImageModalVisible] = useState(false); + + /* eslint-disable @typescript-eslint/naming-convention */ + const classes = classnames('indexPatternFieldEditor__previewFieldList__item', { + 'indexPatternFieldEditor__previewFieldList__item--highlighted': highlighted, + 'indexPatternFieldEditor__previewFieldList__item--pinned': isPinned, + }); + /* eslint-enable @typescript-eslint/naming-convention */ + + const doesContainImage = formattedValue?.includes(' { + if (doesContainImage) { + return ( + setIsPreviewImageModalVisible(true)} + iconType="image" + > + {i18n.translate('indexPatternFieldEditor.fieldPreview.viewImageButtonLabel', { + defaultMessage: 'View image', + })} + + ); + } + + if (formattedValue !== undefined) { + return ( + + ); + } + + return ( + + {JSON.stringify(value)} + + ); + }; + + return ( + <> + + +
+ {key} +
+
+ + + {renderValue()} + + + + + {toggleIsPinned && ( + { + toggleIsPinned(key); + }} + color="text" + iconType="pinFilled" + data-test-subj="pinFieldButton" + aria-label={i18n.translate( + 'indexPatternFieldEditor.fieldPreview.pinFieldButtonLabel', + { + defaultMessage: 'Pin field', + } + )} + className="indexPatternFieldEditor__previewFieldList__item__actionsBtn" + /> + )} + +
+ {isPreviewImageModalVisible && ( + setIsPreviewImageModalVisible(false)} + /> + )} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.scss b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.scss new file mode 100644 index 0000000000000..2d51cd19bf925 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.scss @@ -0,0 +1,19 @@ +.indexPatternFieldEditor { + &__previewPannel { + display: flex; + flex-direction: column; + height: 100%; + } + + &__previewImageModal__wrapper { + padding: $euiSize; + + img { + max-width: 100%; + } + } + + &__previewEmptySearchResult__title { + font-weight: 400; + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.tsx new file mode 100644 index 0000000000000..09bacf2a46096 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiResizeObserver, EuiFieldSearch } from '@elastic/eui'; + +import { useFieldPreviewContext } from './field_preview_context'; +import { FieldPreviewHeader } from './field_preview_header'; +import { FieldPreviewEmptyPrompt } from './field_preview_empty_prompt'; +import { DocumentsNavPreview } from './documents_nav_preview'; +import { FieldPreviewError } from './field_preview_error'; +import { PreviewListItem } from './field_list/field_list_item'; +import { PreviewFieldList } from './field_list/field_list'; + +import './field_preview.scss'; + +export const FieldPreview = () => { + const [fieldListHeight, setFieldListHeight] = useState(-1); + const [searchValue, setSearchValue] = useState(''); + + const { + params: { + value: { name, script, format }, + }, + fields, + error, + reset, + } = useFieldPreviewContext(); + + // To show the preview we at least need a name to be defined, the script or the format + // and an first response from the _execute API + const isEmptyPromptVisible = + name === null && script === null && format === null + ? true + : // If we have some result from the _execute API call don't show the empty prompt + error !== null || fields.length > 0 + ? false + : name === null && format === null + ? true + : false; + + const onFieldListResize = useCallback(({ height }: { height: number }) => { + setFieldListHeight(height); + }, []); + + const renderFieldsToPreview = () => { + if (fields.length === 0) { + return null; + } + + const [field] = fields; + + return ( +
    +
  • + +
  • +
+ ); + }; + + useEffect(() => { + // When unmounting the preview pannel we make sure to reset + // the state of the preview panel. + return reset; + }, [reset]); + + const doShowFieldList = + error === null || (error.code !== 'DOC_NOT_FOUND' && error.code !== 'ERR_FETCHING_DOC'); + + return ( +
+ {isEmptyPromptVisible ? ( + + ) : ( + <> + + + + + + + setSearchValue(e.target.value)} + placeholder={i18n.translate( + 'indexPatternFieldEditor.fieldPreview.filterFieldsPlaceholder', + { + defaultMessage: 'Filter fields', + } + )} + fullWidth + data-test-subj="filterFieldsInput" + /> + + + + + + {doShowFieldList && ( + <> + {/* The current field(s) the user is creating */} + {renderFieldsToPreview()} + + {/* List of other fields in the document */} + + {(resizeRef) => ( +
+ setSearchValue('')} + searchValue={searchValue} + // We add a key to force rerender the virtual list whenever the window height changes + key={fieldListHeight} + /> +
+ )} +
+ + )} + + )} +
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx new file mode 100644 index 0000000000000..e1fc4b05883f4 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx @@ -0,0 +1,599 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { + createContext, + useState, + useContext, + useMemo, + useCallback, + useEffect, + useRef, + FunctionComponent, +} from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; + +import type { FieldPreviewContext, FieldFormatConfig } from '../../types'; +import { parseEsError } from '../../lib/runtime_field_validation'; +import { RuntimeType, RuntimeField } from '../../shared_imports'; +import { useFieldEditorContext } from '../field_editor_context'; + +type From = 'cluster' | 'custom'; +interface EsDocument { + _id: string; + [key: string]: any; +} + +interface PreviewError { + code: 'DOC_NOT_FOUND' | 'PAINLESS_SCRIPT_ERROR' | 'ERR_FETCHING_DOC'; + error: Record; +} + +interface ClusterData { + documents: EsDocument[]; + currentIdx: number; +} + +// The parameters required to preview the field +interface Params { + name: string | null; + index: string | null; + type: RuntimeType | null; + script: Required['script'] | null; + format: FieldFormatConfig | null; + document: EsDocument | null; +} + +export interface FieldPreview { + key: string; + value: unknown; + formattedValue?: string; +} + +interface Context { + fields: FieldPreview[]; + error: PreviewError | null; + params: { + value: Params; + update: (updated: Partial) => void; + }; + isLoadingPreview: boolean; + currentDocument: { + value?: EsDocument; + id: string; + isLoading: boolean; + isCustomId: boolean; + }; + documents: { + loadSingle: (id: string) => void; + loadFromCluster: () => Promise; + }; + panel: { + isVisible: boolean; + setIsVisible: (isVisible: boolean) => void; + }; + from: { + value: From; + set: (value: From) => void; + }; + navigation: { + isFirstDoc: boolean; + isLastDoc: boolean; + next: () => void; + prev: () => void; + }; + reset: () => void; + pinnedFields: { + value: { [key: string]: boolean }; + set: React.Dispatch>; + }; +} + +const fieldPreviewContext = createContext(undefined); + +const defaultParams: Params = { + name: null, + index: null, + script: null, + document: null, + type: null, + format: null, +}; + +export const defaultValueFormatter = (value: unknown) => + `${typeof value === 'object' ? JSON.stringify(value) : value ?? '-'}`; + +export const FieldPreviewProvider: FunctionComponent = ({ children }) => { + const previewCount = useRef(0); + const [lastExecutePainlessRequestParams, setLastExecutePainlessReqParams] = useState<{ + type: Params['type']; + script: string | undefined; + documentId: string | undefined; + }>({ + type: null, + script: undefined, + documentId: undefined, + }); + + const { + indexPattern, + fieldTypeToProcess, + services: { + search, + notifications, + api: { getFieldPreview }, + }, + fieldFormats, + } = useFieldEditorContext(); + + /** Response from the Painless _execute API */ + const [previewResponse, setPreviewResponse] = useState<{ + fields: Context['fields']; + error: Context['error']; + }>({ fields: [], error: null }); + /** The parameters required for the Painless _execute API */ + const [params, setParams] = useState(defaultParams); + /** The sample documents fetched from the cluster */ + const [clusterData, setClusterData] = useState({ + documents: [], + currentIdx: 0, + }); + /** Flag to show/hide the preview panel */ + const [isPanelVisible, setIsPanelVisible] = useState(false); + /** Flag to indicate if we are loading document from cluster */ + const [isFetchingDocument, setIsFetchingDocument] = useState(false); + /** Flag to indicate if we are calling the _execute API */ + const [isLoadingPreview, setIsLoadingPreview] = useState(false); + /** Flag to indicate if we are loading a single document by providing its ID */ + const [customDocIdToLoad, setCustomDocIdToLoad] = useState(null); + /** Define if we provide the document to preview from the cluster or from a custom JSON */ + const [from, setFrom] = useState('cluster'); + /** Map of fields pinned to the top of the list */ + const [pinnedFields, setPinnedFields] = useState<{ [key: string]: boolean }>({}); + + const { documents, currentIdx } = clusterData; + const currentDocument: EsDocument | undefined = useMemo(() => documents[currentIdx], [ + documents, + currentIdx, + ]); + + const currentDocIndex = currentDocument?._index; + const currentDocId: string = currentDocument?._id ?? ''; + const totalDocs = documents.length; + const { name, document, script, format, type } = params; + + const updateParams: Context['params']['update'] = useCallback((updated) => { + setParams((prev) => ({ ...prev, ...updated })); + }, []); + + const needToUpdatePreview = useMemo(() => { + const isCurrentDocIdDefined = currentDocId !== ''; + + if (!isCurrentDocIdDefined) { + return false; + } + + const allParamsDefined = (['type', 'script', 'index', 'document'] as Array< + keyof Params + >).every((key) => Boolean(params[key])); + + if (!allParamsDefined) { + return false; + } + + const hasSomeParamsChanged = + lastExecutePainlessRequestParams.type !== type || + lastExecutePainlessRequestParams.script !== script?.source || + lastExecutePainlessRequestParams.documentId !== currentDocId; + + return hasSomeParamsChanged; + }, [type, script?.source, currentDocId, params, lastExecutePainlessRequestParams]); + + const valueFormatter = useCallback( + (value: unknown) => { + if (format?.id) { + const formatter = fieldFormats.getInstance(format.id, format.params); + if (formatter) { + return formatter.convertObject?.html(value) ?? JSON.stringify(value); + } + } + + return defaultValueFormatter(value); + }, + [format, fieldFormats] + ); + + const fetchSampleDocuments = useCallback( + async (limit: number = 50) => { + if (typeof limit !== 'number') { + // We guard ourself from passing an event accidentally + throw new Error('The "limit" option must be a number'); + } + + setIsFetchingDocument(true); + setClusterData({ + documents: [], + currentIdx: 0, + }); + setPreviewResponse({ fields: [], error: null }); + + const [response, error] = await search + .search({ + params: { + index: indexPattern.title, + body: { + size: limit, + }, + }, + }) + .toPromise() + .then((res) => [res, null]) + .catch((err) => [null, err]); + + setIsFetchingDocument(false); + setCustomDocIdToLoad(null); + + setClusterData({ + documents: response ? response.rawResponse.hits.hits : [], + currentIdx: 0, + }); + + setPreviewResponse((prev) => ({ ...prev, error })); + }, + [indexPattern, search] + ); + + const loadDocument = useCallback( + async (id: string) => { + if (!Boolean(id.trim())) { + return; + } + + setIsFetchingDocument(true); + + const [response, searchError] = await search + .search({ + params: { + index: indexPattern.title, + body: { + size: 1, + query: { + ids: { + values: [id], + }, + }, + }, + }, + }) + .toPromise() + .then((res) => [res, null]) + .catch((err) => [null, err]); + + setIsFetchingDocument(false); + + const isDocumentFound = response?.rawResponse.hits.total > 0; + const loadedDocuments: EsDocument[] = isDocumentFound ? response.rawResponse.hits.hits : []; + const error: Context['error'] = Boolean(searchError) + ? { + code: 'ERR_FETCHING_DOC', + error: { + message: searchError.toString(), + }, + } + : isDocumentFound === false + ? { + code: 'DOC_NOT_FOUND', + error: { + message: i18n.translate( + 'indexPatternFieldEditor.fieldPreview.error.documentNotFoundDescription', + { + defaultMessage: 'Document ID not found', + } + ), + }, + } + : null; + + setPreviewResponse((prev) => ({ ...prev, error })); + + setClusterData({ + documents: loadedDocuments, + currentIdx: 0, + }); + + if (error !== null) { + // Make sure we disable the "Updating..." indicator as we have an error + // and we won't fetch the preview + setIsLoadingPreview(false); + } + }, + [indexPattern, search] + ); + + const updatePreview = useCallback(async () => { + setLastExecutePainlessReqParams({ + type: params.type, + script: params.script?.source, + documentId: currentDocId, + }); + + if (!needToUpdatePreview) { + return; + } + + const currentApiCall = ++previewCount.current; + + const response = await getFieldPreview({ + index: currentDocIndex, + document: params.document!, + context: `${params.type!}_field` as FieldPreviewContext, + script: params.script!, + }); + + if (currentApiCall !== previewCount.current) { + // Discard this response as there is another one inflight + // or we have called reset() and don't need the response anymore. + return; + } + + setIsLoadingPreview(false); + + const { error: serverError } = response; + + if (serverError) { + // Server error (not an ES error) + const title = i18n.translate('indexPatternFieldEditor.fieldPreview.errorTitle', { + defaultMessage: 'Failed to load field preview', + }); + notifications.toasts.addError(serverError, { title }); + + return; + } + + const { values, error } = response.data ?? { values: [], error: {} }; + + if (error) { + const fallBackError = { + message: i18n.translate('indexPatternFieldEditor.fieldPreview.defaultErrorTitle', { + defaultMessage: 'Unable to run the provided script', + }), + }; + + setPreviewResponse({ + fields: [], + error: { code: 'PAINLESS_SCRIPT_ERROR', error: parseEsError(error, true) ?? fallBackError }, + }); + } else { + const [value] = values; + const formattedValue = valueFormatter(value); + + setPreviewResponse({ + fields: [{ key: params.name!, value, formattedValue }], + error: null, + }); + } + }, [ + needToUpdatePreview, + params, + currentDocIndex, + currentDocId, + getFieldPreview, + notifications.toasts, + valueFormatter, + ]); + + const goToNextDoc = useCallback(() => { + if (currentIdx >= totalDocs - 1) { + setClusterData((prev) => ({ ...prev, currentIdx: 0 })); + } else { + setClusterData((prev) => ({ ...prev, currentIdx: prev.currentIdx + 1 })); + } + }, [currentIdx, totalDocs]); + + const goToPrevDoc = useCallback(() => { + if (currentIdx === 0) { + setClusterData((prev) => ({ ...prev, currentIdx: totalDocs - 1 })); + } else { + setClusterData((prev) => ({ ...prev, currentIdx: prev.currentIdx - 1 })); + } + }, [currentIdx, totalDocs]); + + const reset = useCallback(() => { + // By resetting the previewCount we will discard any inflight + // API call response coming in after calling reset() was called + previewCount.current = 0; + + setClusterData({ + documents: [], + currentIdx: 0, + }); + setPreviewResponse({ fields: [], error: null }); + setLastExecutePainlessReqParams({ + type: null, + script: undefined, + documentId: undefined, + }); + setFrom('cluster'); + setIsLoadingPreview(false); + setIsFetchingDocument(false); + }, []); + + const ctx = useMemo( + () => ({ + fields: previewResponse.fields, + error: previewResponse.error, + isLoadingPreview, + params: { + value: params, + update: updateParams, + }, + currentDocument: { + value: currentDocument, + id: customDocIdToLoad !== null ? customDocIdToLoad : currentDocId, + isLoading: isFetchingDocument, + isCustomId: customDocIdToLoad !== null, + }, + documents: { + loadSingle: setCustomDocIdToLoad, + loadFromCluster: fetchSampleDocuments, + }, + navigation: { + isFirstDoc: currentIdx === 0, + isLastDoc: currentIdx >= totalDocs - 1, + next: goToNextDoc, + prev: goToPrevDoc, + }, + panel: { + isVisible: isPanelVisible, + setIsVisible: setIsPanelVisible, + }, + from: { + value: from, + set: setFrom, + }, + reset, + pinnedFields: { + value: pinnedFields, + set: setPinnedFields, + }, + }), + [ + previewResponse, + params, + isLoadingPreview, + updateParams, + currentDocument, + currentDocId, + fetchSampleDocuments, + isFetchingDocument, + customDocIdToLoad, + currentIdx, + totalDocs, + goToNextDoc, + goToPrevDoc, + isPanelVisible, + from, + reset, + pinnedFields, + ] + ); + + /** + * In order to immediately display the "Updating..." state indicator and not have to wait + * the 500ms of the debounce, we set the isLoadingPreview state in this effect + */ + useEffect(() => { + if (needToUpdatePreview) { + setIsLoadingPreview(true); + } + }, [needToUpdatePreview, customDocIdToLoad]); + + /** + * Whenever we enter manually a document ID to load we'll clear the + * documents and the preview value. + */ + useEffect(() => { + if (customDocIdToLoad !== null) { + setIsFetchingDocument(true); + + setClusterData({ + documents: [], + currentIdx: 0, + }); + + setPreviewResponse((prev) => { + const { + fields: { 0: field }, + } = prev; + return { + ...prev, + fields: [ + { ...field, value: undefined, formattedValue: defaultValueFormatter(undefined) }, + ], + }; + }); + } + }, [customDocIdToLoad]); + + /** + * Whenever we show the preview panel we will update the documents from the cluster + */ + useEffect(() => { + if (isPanelVisible) { + fetchSampleDocuments(); + } + }, [isPanelVisible, fetchSampleDocuments, fieldTypeToProcess]); + + /** + * Each time the current document changes we update the parameters + * that will be sent in the _execute HTTP request. + */ + useEffect(() => { + updateParams({ + document: currentDocument?._source, + index: currentDocument?._index, + }); + }, [currentDocument, updateParams]); + + /** + * Whenever the name or the format changes we immediately update the preview + */ + useEffect(() => { + setPreviewResponse((prev) => { + const { + fields: { 0: field }, + } = prev; + + const nextValue = + script === null && Boolean(document) + ? get(document, name ?? '') // When there is no script we read the value from _source + : field?.value; + + const formattedValue = valueFormatter(nextValue); + + return { + ...prev, + fields: [{ ...field, key: name ?? '', value: nextValue, formattedValue }], + }; + }); + }, [name, script, document, valueFormatter]); + + useDebounce( + // Whenever updatePreview() changes (meaning whenever any of the params changes) + // we call it to update the preview response with the field(s) value or possible error. + updatePreview, + 500, + [updatePreview] + ); + + useDebounce( + () => { + if (customDocIdToLoad === null) { + return; + } + + loadDocument(customDocIdToLoad); + }, + 500, + [customDocIdToLoad] + ); + + return {children}; +}; + +export const useFieldPreviewContext = (): Context => { + const ctx = useContext(fieldPreviewContext); + + if (ctx === undefined) { + throw new Error('useFieldPreviewContext must be used within a '); + } + + return ctx; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_empty_prompt.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_empty_prompt.tsx new file mode 100644 index 0000000000000..6e4c4626d9dae --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_empty_prompt.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiEmptyPrompt, EuiText, EuiTextColor, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export const FieldPreviewEmptyPrompt = () => { + return ( + + + + {i18n.translate('indexPatternFieldEditor.fieldPreview.emptyPromptTitle', { + defaultMessage: 'Preview', + })} + + } + titleSize="s" + body={ + + +

+ {i18n.translate('indexPatternFieldEditor.fieldPreview.emptyPromptDescription', { + defaultMessage: + 'Enter the name of an existing field or define a script to view a preview of the calculated output.', + })} +

+
+
+ } + /> +
+
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_error.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_error.tsx new file mode 100644 index 0000000000000..7994e649e1ebb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_error.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useFieldPreviewContext } from './field_preview_context'; + +export const FieldPreviewError = () => { + const { error } = useFieldPreviewContext(); + + if (error === null) { + return null; + } + + return ( + + {error.code === 'PAINLESS_SCRIPT_ERROR' ? ( +

{error.error.reason}

+ ) : ( +

{error.error.message}

+ )} +
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_header.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_header.tsx new file mode 100644 index 0000000000000..2d3d5c20ba7b3 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_header.tsx @@ -0,0 +1,77 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiTitle, + EuiText, + EuiTextColor, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useFieldEditorContext } from '../field_editor_context'; +import { useFieldPreviewContext } from './field_preview_context'; + +const i18nTexts = { + title: i18n.translate('indexPatternFieldEditor.fieldPreview.title', { + defaultMessage: 'Preview', + }), + customData: i18n.translate('indexPatternFieldEditor.fieldPreview.subTitle.customData', { + defaultMessage: 'Custom data', + }), + updatingLabel: i18n.translate('indexPatternFieldEditor.fieldPreview.updatingPreviewLabel', { + defaultMessage: 'Updating...', + }), +}; + +export const FieldPreviewHeader = () => { + const { indexPattern } = useFieldEditorContext(); + const { + from, + isLoadingPreview, + currentDocument: { isLoading }, + } = useFieldPreviewContext(); + + const isUpdating = isLoadingPreview || isLoading; + + return ( +
+ + + +

{i18nTexts.title}

+
+
+ + {isUpdating && ( + + + + + + {i18nTexts.updatingLabel} + + + )} +
+ + + {i18n.translate('indexPatternFieldEditor.fieldPreview.subTitle', { + defaultMessage: 'From: {from}', + values: { + from: from.value === 'cluster' ? indexPattern.title : i18nTexts.customData, + }, + })} + + +
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/image_preview_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/image_preview_modal.tsx new file mode 100644 index 0000000000000..69be3d144bfda --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/image_preview_modal.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiModal, EuiModalBody } from '@elastic/eui'; + +/** + * By default the image formatter sets the max-width to "none" on the tag + * To render nicely the image in the modal we want max_width: 100% + */ +const setMaxWidthImage = (imgHTML: string): string => { + const regex = new RegExp('max-width:[^;]+;', 'gm'); + + if (regex.test(imgHTML)) { + return imgHTML.replace(regex, 'max-width: 100%;'); + } + + return imgHTML; +}; + +interface Props { + imgHTML: string; + closeModal: () => void; +} + +export const ImagePreviewModal = ({ imgHTML, closeModal }: Props) => { + return ( + + +
+ + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/index.ts b/src/plugins/index_pattern_field_editor/public/components/preview/index.ts new file mode 100644 index 0000000000000..5d3b4bb41fc5f --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/index.ts @@ -0,0 +1,11 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { useFieldPreviewContext, FieldPreviewProvider } from './field_preview_context'; + +export { FieldPreview } from './field_preview'; diff --git a/src/plugins/index_pattern_field_editor/public/constants.ts b/src/plugins/index_pattern_field_editor/public/constants.ts index 69d231f375848..5a16e805a3fc9 100644 --- a/src/plugins/index_pattern_field_editor/public/constants.ts +++ b/src/plugins/index_pattern_field_editor/public/constants.ts @@ -7,3 +7,5 @@ */ export const pluginName = 'index_pattern_field_editor'; + +export const euiFlyoutClassname = 'indexPatternFieldEditorFlyout'; diff --git a/src/plugins/index_pattern_field_editor/public/index.ts b/src/plugins/index_pattern_field_editor/public/index.ts index a63c9ada52e3d..80ead500c3d9d 100644 --- a/src/plugins/index_pattern_field_editor/public/index.ts +++ b/src/plugins/index_pattern_field_editor/public/index.ts @@ -21,7 +21,7 @@ import { IndexPatternFieldEditorPlugin } from './plugin'; export { PluginStart as IndexPatternFieldEditorStart } from './types'; -export { DefaultFormatEditor } from './components'; +export { DefaultFormatEditor } from './components/field_format_editor/editors/default/default'; export { FieldFormatEditorFactory, FieldFormatEditor, FormatEditorProps } from './components'; export function plugin() { @@ -31,4 +31,3 @@ export function plugin() { // Expose types export type { OpenFieldEditorOptions } from './open_editor'; export type { OpenFieldDeleteModalOptions } from './open_delete_modal'; -export type { FieldEditorContext } from './components/field_editor_flyout_content_container'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/api.ts b/src/plugins/index_pattern_field_editor/public/lib/api.ts new file mode 100644 index 0000000000000..9325b5c2faf47 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/api.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { HttpSetup } from 'src/core/public'; +import { API_BASE_PATH } from '../../common/constants'; +import { sendRequest } from '../shared_imports'; +import { FieldPreviewContext, FieldPreviewResponse } from '../types'; + +export const initApi = (httpClient: HttpSetup) => { + const getFieldPreview = ({ + index, + context, + script, + document, + }: { + index: string; + context: FieldPreviewContext; + script: { source: string } | null; + document: Record; + }) => { + return sendRequest(httpClient, { + path: `${API_BASE_PATH}/field_preview`, + method: 'post', + body: { + index, + context, + script, + document, + }, + }); + }; + + return { + getFieldPreview, + }; +}; + +export type ApiService = ReturnType; diff --git a/src/plugins/index_pattern_field_editor/public/lib/index.ts b/src/plugins/index_pattern_field_editor/public/lib/index.ts index 5d5b3d881e976..336de9574c460 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/index.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/index.ts @@ -10,4 +10,10 @@ export { deserializeField } from './serialization'; export { getLinks } from './documentation'; -export { getRuntimeFieldValidator, RuntimeFieldPainlessError } from './runtime_field_validation'; +export { + getRuntimeFieldValidator, + RuntimeFieldPainlessError, + parseEsError, +} from './runtime_field_validation'; + +export { initApi, ApiService } from './api'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts index f1a6fd7f9e8aa..789c4f7fa71fc 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { DataPublicPluginStart } from '../shared_imports'; -import { EsRuntimeField } from '../types'; +import type { EsRuntimeField } from '../types'; export interface RuntimeFieldPainlessError { message: string; @@ -60,12 +60,15 @@ const getScriptExceptionError = (error: Error): Error | null => { return scriptExceptionError; }; -const parseEsError = (error?: Error): RuntimeFieldPainlessError | null => { +export const parseEsError = ( + error?: Error, + isScriptError = false +): RuntimeFieldPainlessError | null => { if (error === undefined) { return null; } - const scriptError = getScriptExceptionError(error.caused_by); + const scriptError = isScriptError ? error : getScriptExceptionError(error.caused_by); if (scriptError === null) { return null; diff --git a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts index 9000a34b23cbe..8a0a47e07c9c9 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts @@ -7,7 +7,7 @@ */ import { IndexPatternField, IndexPattern } from '../shared_imports'; -import { Field } from '../types'; +import type { Field } from '../types'; export const deserializeField = ( indexPattern: IndexPattern, diff --git a/src/plugins/index_pattern_field_editor/public/open_delete_modal.tsx b/src/plugins/index_pattern_field_editor/public/open_delete_modal.tsx index 27aa1d0313a7f..72dbb76863353 100644 --- a/src/plugins/index_pattern_field_editor/public/open_delete_modal.tsx +++ b/src/plugins/index_pattern_field_editor/public/open_delete_modal.tsx @@ -18,7 +18,7 @@ import { import { CloseEditor } from './types'; -import { DeleteFieldModal } from './components/delete_field_modal'; +import { DeleteFieldModal } from './components/confirm_modals/delete_field_modal'; import { removeFields } from './lib/remove_fields'; export interface OpenFieldDeleteModalOptions { diff --git a/src/plugins/index_pattern_field_editor/public/open_editor.tsx b/src/plugins/index_pattern_field_editor/public/open_editor.tsx index e70ba48cca0a5..946e666bf8205 100644 --- a/src/plugins/index_pattern_field_editor/public/open_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/open_editor.tsx @@ -19,10 +19,10 @@ import { UsageCollectionStart, } from './shared_imports'; -import { InternalFieldType, CloseEditor } from './types'; -import { FieldEditorFlyoutContentContainer } from './components/field_editor_flyout_content_container'; - -import { PluginStart } from './types'; +import type { PluginStart, InternalFieldType, CloseEditor } from './types'; +import type { ApiService } from './lib/api'; +import { euiFlyoutClassname } from './constants'; +import { FieldEditorLoader } from './components/field_editor_loader'; export interface OpenFieldEditorOptions { ctx: { @@ -37,6 +37,7 @@ interface Dependencies { /** The search service from the data plugin */ search: DataPublicPluginStart['search']; indexPatternService: DataPublicPluginStart['indexPatterns']; + apiService: ApiService; fieldFormats: DataPublicPluginStart['fieldFormats']; fieldFormatEditors: PluginStart['fieldFormatEditors']; usageCollection: UsageCollectionStart; @@ -49,6 +50,7 @@ export const getFieldEditorOpener = ({ fieldFormatEditors, search, usageCollection, + apiService, }: Dependencies) => (options: OpenFieldEditorOptions): CloseEditor => { const { uiSettings, overlays, docLinks, notifications } = core; const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ @@ -58,8 +60,19 @@ export const getFieldEditorOpener = ({ }); let overlayRef: OverlayRef | null = null; + const canCloseValidator = { + current: () => true, + }; + + const onMounted = (args: { canCloseValidator: () => boolean }) => { + canCloseValidator.current = args.canCloseValidator; + }; - const openEditor = ({ onSave, fieldName, ctx }: OpenFieldEditorOptions): CloseEditor => { + const openEditor = ({ + onSave, + fieldName, + ctx: { indexPattern }, + }: OpenFieldEditorOptions): CloseEditor => { const closeEditor = () => { if (overlayRef) { overlayRef.close(); @@ -75,7 +88,7 @@ export const getFieldEditorOpener = ({ } }; - const field = fieldName ? ctx.indexPattern.getFieldByName(fieldName) : undefined; + const field = fieldName ? indexPattern.getFieldByName(fieldName) : undefined; if (fieldName && !field) { const err = i18n.translate('indexPatternFieldEditor.noSuchFieldName', { @@ -94,21 +107,48 @@ export const getFieldEditorOpener = ({ overlayRef = overlays.openFlyout( toMountPoint( - - ) + ), + { + className: euiFlyoutClassname, + maxWidth: 708, + size: 'l', + ownFocus: true, + hideCloseButton: true, + 'aria-label': isNewRuntimeField + ? i18n.translate('indexPatternFieldEditor.createField.flyoutAriaLabel', { + defaultMessage: 'Create field', + }) + : i18n.translate('indexPatternFieldEditor.editField.flyoutAriaLabel', { + defaultMessage: 'Edit {fieldName} field', + values: { + fieldName, + }, + }), + onClose: (flyout) => { + const canClose = canCloseValidator.current(); + if (canClose) { + flyout.close(); + } + }, + } ); return closeEditor; diff --git a/src/plugins/index_pattern_field_editor/public/plugin.test.tsx b/src/plugins/index_pattern_field_editor/public/plugin.test.tsx index 2212264427d1a..75bb1322d305e 100644 --- a/src/plugins/index_pattern_field_editor/public/plugin.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/plugin.test.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ import React from 'react'; +import { registerTestBed } from '@kbn/test/jest'; jest.mock('../../kibana_react/public', () => { const original = jest.requireActual('../../kibana_react/public'); @@ -21,11 +22,9 @@ import { coreMock } from 'src/core/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; import { usageCollectionPluginMock } from '../../usage_collection/public/mocks'; -import { registerTestBed } from './test_utils'; - -import { FieldEditorFlyoutContentContainer } from './components/field_editor_flyout_content_container'; +import { FieldEditorLoader } from './components/field_editor_loader'; import { IndexPatternFieldEditorPlugin } from './plugin'; -import { DeleteFieldModal } from './components/delete_field_modal'; +import { DeleteFieldModal } from './components/confirm_modals/delete_field_modal'; import { IndexPattern } from './shared_imports'; const noop = () => {}; @@ -66,7 +65,7 @@ describe('IndexPatternFieldEditorPlugin', () => { expect(openFlyout).toHaveBeenCalled(); const [[arg]] = openFlyout.mock.calls; - expect(arg.props.children.type).toBe(FieldEditorFlyoutContentContainer); + expect(arg.props.children.type).toBe(FieldEditorLoader); // We force call the "onSave" prop from the component // and make sure that the the spy is being called. diff --git a/src/plugins/index_pattern_field_editor/public/plugin.ts b/src/plugins/index_pattern_field_editor/public/plugin.ts index a46bef92cbbb1..4bf8dd5c1c4e8 100644 --- a/src/plugins/index_pattern_field_editor/public/plugin.ts +++ b/src/plugins/index_pattern_field_editor/public/plugin.ts @@ -8,11 +8,12 @@ import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; -import { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types'; +import type { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types'; import { getFieldEditorOpener } from './open_editor'; -import { FormatEditorService } from './service'; +import { FormatEditorService } from './service/format_editor_service'; import { getDeleteFieldProvider } from './components/delete_field_provider'; import { getFieldDeleteModalOpener } from './open_delete_modal'; +import { initApi } from './lib/api'; export class IndexPatternFieldEditorPlugin implements Plugin { @@ -30,6 +31,7 @@ export class IndexPatternFieldEditorPlugin const { fieldFormatEditors } = this.formatEditorService.start(); const { application: { capabilities }, + http, } = core; const { data, usageCollection } = plugins; const openDeleteModal = getFieldDeleteModalOpener({ @@ -42,6 +44,7 @@ export class IndexPatternFieldEditorPlugin openEditor: getFieldEditorOpener({ core, indexPatternService: data.indexPatterns, + apiService: initApi(http), fieldFormats: data.fieldFormats, fieldFormatEditors, search: data.search, diff --git a/src/plugins/index_pattern_field_editor/public/shared_imports.ts b/src/plugins/index_pattern_field_editor/public/shared_imports.ts index cfc36543780c1..2827928d1c060 100644 --- a/src/plugins/index_pattern_field_editor/public/shared_imports.ts +++ b/src/plugins/index_pattern_field_editor/public/shared_imports.ts @@ -20,6 +20,7 @@ export { useForm, useFormData, useFormContext, + useFormIsModified, Form, FormSchema, UseField, @@ -31,3 +32,5 @@ export { export { fieldValidators } from '../../es_ui_shared/static/forms/helpers'; export { TextField, ToggleField, NumericField } from '../../es_ui_shared/static/forms/components'; + +export { sendRequest } from '../../es_ui_shared/public'; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts deleted file mode 100644 index b55a59df34545..0000000000000 --- a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { act } from 'react-dom/test-utils'; -import { TestBed } from './test_utils'; - -export const getCommonActions = (testBed: TestBed) => { - const toggleFormRow = (row: 'customLabel' | 'value' | 'format', value: 'on' | 'off' = 'on') => { - const testSubj = `${row}Row.toggle`; - const toggle = testBed.find(testSubj); - const isOn = toggle.props()['aria-checked']; - - if ((value === 'on' && isOn) || (value === 'off' && isOn === false)) { - return; - } - - testBed.form.toggleEuiSwitch(testSubj); - }; - - const changeFieldType = async (value: string, label?: string) => { - await act(async () => { - testBed.find('typeField').simulate('change', [ - { - value, - label: label ?? value, - }, - ]); - }); - testBed.component.update(); - }; - - return { - toggleFormRow, - changeFieldType, - }; -}; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts b/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts deleted file mode 100644 index c6bc24f176858..0000000000000 --- a/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { DocLinksStart } from 'src/core/public'; - -export const noop = () => {}; - -export const docLinks: DocLinksStart = { - ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', - DOC_LINK_VERSION: 'jest', - links: {} as any, -}; - -// TODO check how we can better stub an index pattern format -export const fieldFormats = { - getDefaultInstance: () => ({ - convert: (val: any) => val, - }), -} as any; diff --git a/src/plugins/index_pattern_field_editor/public/types.ts b/src/plugins/index_pattern_field_editor/public/types.ts index e78c0805c51b5..f7efc9d82fc48 100644 --- a/src/plugins/index_pattern_field_editor/public/types.ts +++ b/src/plugins/index_pattern_field_editor/public/types.ts @@ -65,3 +65,17 @@ export interface EsRuntimeField { } export type CloseEditor = () => void; + +export type FieldPreviewContext = + | 'boolean_field' + | 'date_field' + | 'double_field' + | 'geo_point_field' + | 'ip_field' + | 'keyword_field' + | 'long_field'; + +export interface FieldPreviewResponse { + values: unknown[]; + error?: Record; +} diff --git a/src/plugins/index_pattern_field_editor/server/index.ts b/src/plugins/index_pattern_field_editor/server/index.ts new file mode 100644 index 0000000000000..dc6f734a7e503 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/server/index.ts @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { PluginInitializerContext } from '../../../../src/core/server'; +import { IndexPatternPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new IndexPatternPlugin(initializerContext); +} diff --git a/src/plugins/index_pattern_field_editor/server/plugin.ts b/src/plugins/index_pattern_field_editor/server/plugin.ts new file mode 100644 index 0000000000000..18601aad85307 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/server/plugin.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server'; + +import { ApiRoutes } from './routes'; + +export class IndexPatternPlugin implements Plugin { + private readonly logger: Logger; + private readonly apiRoutes: ApiRoutes; + + constructor({ logger }: PluginInitializerContext) { + this.logger = logger.get(); + this.apiRoutes = new ApiRoutes(); + } + + public setup({ http }: CoreSetup) { + this.logger.debug('index_pattern_field_editor: setup'); + + const router = http.createRouter(); + this.apiRoutes.setup({ router }); + } + + public start() {} + + public stop() {} +} diff --git a/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts new file mode 100644 index 0000000000000..238701904e22c --- /dev/null +++ b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts @@ -0,0 +1,73 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { HttpResponsePayload } from 'kibana/server'; + +import { API_BASE_PATH } from '../../common/constants'; +import { RouteDependencies } from '../types'; +import { handleEsError } from '../shared_imports'; + +const bodySchema = schema.object({ + index: schema.string(), + script: schema.object({ source: schema.string() }), + context: schema.oneOf([ + schema.literal('boolean_field'), + schema.literal('date_field'), + schema.literal('double_field'), + schema.literal('geo_point_field'), + schema.literal('ip_field'), + schema.literal('keyword_field'), + schema.literal('long_field'), + ]), + document: schema.object({}, { unknowns: 'allow' }), +}); + +export const registerFieldPreviewRoute = ({ router }: RouteDependencies): void => { + router.post( + { + path: `${API_BASE_PATH}/field_preview`, + validate: { + body: bodySchema, + }, + }, + async (ctx, req, res) => { + const { client } = ctx.core.elasticsearch; + + const body = JSON.stringify({ + script: req.body.script, + context: req.body.context, + context_setup: { + document: req.body.document, + index: req.body.index, + } as any, + }); + + try { + const response = await client.asCurrentUser.scriptsPainlessExecute({ + // @ts-expect-error `ExecutePainlessScriptRequest.body` does not allow `string` + body, + }); + + const fieldValue = (response.body.result as any[]) as HttpResponsePayload; + + return res.ok({ body: { values: fieldValue } }); + } catch (error) { + // Assume invalid painless script was submitted + // Return 200 with error object + const handleCustomError = () => { + return res.ok({ + body: { values: [], ...error.body }, + }); + }; + + return handleEsError({ error, response: res, handleCustomError }); + } + } + ); +}; diff --git a/src/plugins/index_pattern_field_editor/server/routes/index.ts b/src/plugins/index_pattern_field_editor/server/routes/index.ts new file mode 100644 index 0000000000000..c04c5bb3feec4 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/server/routes/index.ts @@ -0,0 +1,16 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RouteDependencies } from '../types'; +import { registerFieldPreviewRoute } from './field_preview'; + +export class ApiRoutes { + setup(dependencies: RouteDependencies) { + registerFieldPreviewRoute(dependencies); + } +} diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts b/src/plugins/index_pattern_field_editor/server/shared_imports.ts similarity index 76% rename from src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts rename to src/plugins/index_pattern_field_editor/server/shared_imports.ts index c8e4aedc26471..d818f11ceefda 100644 --- a/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts +++ b/src/plugins/index_pattern_field_editor/server/shared_imports.ts @@ -6,6 +6,4 @@ * Side Public License, v 1. */ -export { getRandomString } from '@kbn/test/jest'; - -export { registerTestBed, TestBed } from '@kbn/test/jest'; +export { handleEsError } from '../../es_ui_shared/server'; diff --git a/src/plugins/index_pattern_field_editor/server/types.ts b/src/plugins/index_pattern_field_editor/server/types.ts new file mode 100644 index 0000000000000..c86708c12a71e --- /dev/null +++ b/src/plugins/index_pattern_field_editor/server/types.ts @@ -0,0 +1,12 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { IRouter } from 'src/core/server'; + +export interface RouteDependencies { + router: IRouter; +} diff --git a/src/plugins/index_pattern_field_editor/tsconfig.json b/src/plugins/index_pattern_field_editor/tsconfig.json index e5caf463835d0..11a16ace1f2f5 100644 --- a/src/plugins/index_pattern_field_editor/tsconfig.json +++ b/src/plugins/index_pattern_field_editor/tsconfig.json @@ -7,7 +7,11 @@ "declarationMap": true }, "include": [ + "../../../typings/**/*", + "__jest__/**/*", + "common/**/*", "public/**/*", + "server/**/*", ], "references": [ { "path": "../../core/tsconfig.json" }, diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index 538755b482fbf..2fb3de63a81a7 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -5,11 +5,16 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'settings', 'header']); + const PageObjects = getPageObjects([ + 'common', + 'settings', + 'header', + 'indexPatternFieldEditorObjects', + ]); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const a11y = getService('a11y'); @@ -58,10 +63,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.toggleRow('customLabelRow'); await PageObjects.settings.setCustomLabel('custom label'); await testSubjects.click('toggleAdvancedSetting'); + // Let's make sure the field preview is visible before testing the snapshot + const isFieldPreviewVisible = await PageObjects.indexPatternFieldEditorObjects.isFieldPreviewVisible(); + expect(isFieldPreviewVisible).to.be(true); await a11y.testAppSnapshot(); - await testSubjects.click('euiFlyoutCloseButton'); await PageObjects.settings.closeIndexPatternFieldEditor(); }); @@ -83,7 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Edit field type', async () => { await PageObjects.settings.clickEditFieldFormat(); await a11y.testAppSnapshot(); - await PageObjects.settings.clickCloseEditFieldFormatFlyout(); + await PageObjects.settings.closeIndexPatternFieldEditor(); }); it('Advanced settings', async () => { diff --git a/test/api_integration/apis/index.ts b/test/api_integration/apis/index.ts index 0d87569cb8b97..998c0b834d224 100644 --- a/test/api_integration/apis/index.ts +++ b/test/api_integration/apis/index.ts @@ -14,6 +14,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./core')); loadTestFile(require.resolve('./general')); loadTestFile(require.resolve('./home')); + loadTestFile(require.resolve('./index_pattern_field_editor')); loadTestFile(require.resolve('./index_patterns')); loadTestFile(require.resolve('./kql_telemetry')); loadTestFile(require.resolve('./saved_objects_management')); diff --git a/test/api_integration/apis/index_pattern_field_editor/constants.ts b/test/api_integration/apis/index_pattern_field_editor/constants.ts new file mode 100644 index 0000000000000..ecd6b1ddd408b --- /dev/null +++ b/test/api_integration/apis/index_pattern_field_editor/constants.ts @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const API_BASE_PATH = '/api/index_pattern_field_editor'; diff --git a/test/api_integration/apis/index_pattern_field_editor/field_preview.ts b/test/api_integration/apis/index_pattern_field_editor/field_preview.ts new file mode 100644 index 0000000000000..a84accc8e5f03 --- /dev/null +++ b/test/api_integration/apis/index_pattern_field_editor/field_preview.ts @@ -0,0 +1,130 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { API_BASE_PATH } from './constants'; + +const INDEX_NAME = 'api-integration-test-field-preview'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + + const createIndex = async () => { + await es.indices.create({ + index: INDEX_NAME, + body: { + mappings: { + properties: { + foo: { + type: 'integer', + }, + bar: { + type: 'keyword', + }, + }, + }, + }, + }); + }; + + const deleteIndex = async () => { + await es.indices.delete({ + index: INDEX_NAME, + }); + }; + + describe('Field preview', function () { + before(async () => await createIndex()); + after(async () => await deleteIndex()); + + describe('should return the script value', () => { + const document = { foo: 1, bar: 'hello' }; + + const tests = [ + { + context: 'keyword_field', + script: { + source: 'emit("test")', + }, + expected: 'test', + }, + { + context: 'long_field', + script: { + source: 'emit(doc["foo"].value + 1)', + }, + expected: 2, + }, + { + context: 'keyword_field', + script: { + source: 'emit(doc["bar"].value + " world")', + }, + expected: 'hello world', + }, + ]; + + tests.forEach((test) => { + it(`> ${test.context}`, async () => { + const payload = { + script: test.script, + document, + context: test.context, + index: INDEX_NAME, + }; + + const { body: response } = await supertest + .post(`${API_BASE_PATH}/field_preview`) + .send(payload) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(response.values).eql([test.expected]); + }); + }); + }); + + describe('payload validation', () => { + it('should require a script', async () => { + await supertest + .post(`${API_BASE_PATH}/field_preview`) + .send({ + context: 'keyword_field', + index: INDEX_NAME, + }) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + + it('should require a context', async () => { + await supertest + .post(`${API_BASE_PATH}/field_preview`) + .send({ + script: { source: 'emit("hello")' }, + index: INDEX_NAME, + }) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + + it('should require an index', async () => { + await supertest + .post(`${API_BASE_PATH}/field_preview`) + .send({ + script: { source: 'emit("hello")' }, + context: 'keyword_field', + }) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); + }); + }); +} diff --git a/test/api_integration/apis/index_pattern_field_editor/index.ts b/test/api_integration/apis/index_pattern_field_editor/index.ts new file mode 100644 index 0000000000000..51e4dfaa6ccee --- /dev/null +++ b/test/api_integration/apis/index_pattern_field_editor/index.ts @@ -0,0 +1,15 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('index pattern field editor', () => { + loadTestFile(require.resolve('./field_preview')); + }); +} diff --git a/test/functional/apps/management/_field_formatter.ts b/test/functional/apps/management/_field_formatter.ts index 60c1bbe7b3d1d..9231da8209326 100644 --- a/test/functional/apps/management/_field_formatter.ts +++ b/test/functional/apps/management/_field_formatter.ts @@ -53,7 +53,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.setFieldFormat('duration'); await PageObjects.settings.setFieldFormat('bytes'); await PageObjects.settings.setFieldFormat('duration'); - await testSubjects.click('euiFlyoutCloseButton'); await PageObjects.settings.closeIndexPatternFieldEditor(); }); }); diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.js index 105e1a394fecb..9a051bbdef6eb 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.js @@ -17,8 +17,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const testSubjects = getService('testSubjects'); - // Failing: See https://github.com/elastic/kibana/issues/95376 - describe.skip('runtime fields', function () { + describe('runtime fields', function () { this.tags(['skipFirefox']); before(async function () { @@ -44,7 +43,18 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickIndexPatternLogstash(); const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount()); await log.debug('add runtime field'); - await PageObjects.settings.addRuntimeField(fieldName, 'Keyword', "emit('hello world')"); + await PageObjects.settings.addRuntimeField( + fieldName, + 'Keyword', + "emit('hello world')", + false + ); + + await log.debug('check that field preview is rendered'); + expect(await testSubjects.exists('fieldPreviewItem', { timeout: 1500 })).to.be(true); + + await PageObjects.settings.clickSaveField(); + await retry.try(async function () { expect(parseInt(await PageObjects.settings.getFieldsTabCount())).to.be(startingCount + 1); }); diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 7c06344c1a1ad..4c9cb150eca03 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -30,6 +30,7 @@ import { TagCloudPageObject } from './tag_cloud_page'; import { VegaChartPageObject } from './vega_chart_page'; import { SavedObjectsPageObject } from './management/saved_objects_page'; import { LegacyDataTableVisPageObject } from './legacy/data_table_vis'; +import { IndexPatternFieldEditorPageObject } from './management/indexpattern_field_editor_page'; export const pageObjects = { common: CommonPageObject, @@ -56,4 +57,5 @@ export const pageObjects = { tagCloud: TagCloudPageObject, vegaChart: VegaChartPageObject, savedObjects: SavedObjectsPageObject, + indexPatternFieldEditorObjects: IndexPatternFieldEditorPageObject, }; diff --git a/test/functional/page_objects/management/indexpattern_field_editor_page.ts b/test/functional/page_objects/management/indexpattern_field_editor_page.ts new file mode 100644 index 0000000000000..6e122b1da5da2 --- /dev/null +++ b/test/functional/page_objects/management/indexpattern_field_editor_page.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrService } from '../../ftr_provider_context'; + +export class IndexPatternFieldEditorPageObject extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + + public async isFieldPreviewVisible() { + this.log.debug('isFieldPreviewVisible'); + return await this.testSubjects.exists('fieldPreviewItem', { timeout: 1500 }); + } +} diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 5c51a8e76dcad..2645148467d58 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -217,7 +217,9 @@ export class SettingsPageObject extends FtrService { async getFieldsTabCount() { return this.retry.try(async () => { + // We extract the text from the tab (something like "Fields (86)") const text = await this.testSubjects.getVisibleText('tab-indexedFields'); + // And we return the number inside the parenthesis "86" return text.split(' ')[1].replace(/\((.*)\)/, '$1'); }); } @@ -543,15 +545,16 @@ export class SettingsPageObject extends FtrService { await this.clickSaveScriptedField(); } - async addRuntimeField(name: string, type: string, script: string) { + async addRuntimeField(name: string, type: string, script: string, doSaveField = true) { await this.clickAddField(); await this.setFieldName(name); await this.setFieldType(type); if (script) { await this.setFieldScript(script); } - await this.clickSaveField(); - await this.closeIndexPatternFieldEditor(); + if (doSaveField) { + await this.clickSaveField(); + } } public async confirmSave() { @@ -565,8 +568,16 @@ export class SettingsPageObject extends FtrService { } async closeIndexPatternFieldEditor() { + await this.testSubjects.click('closeFlyoutButton'); + + // We might have unsaved changes and we need to confirm inside the modal + if (await this.testSubjects.exists('runtimeFieldModifiedFieldConfirmModal')) { + this.log.debug('Unsaved changes for the field: need to confirm'); + await this.testSubjects.click('confirmModalConfirmButton'); + } + await this.retry.waitFor('field editor flyout to close', async () => { - return !(await this.testSubjects.exists('euiFlyoutCloseButton')); + return !(await this.testSubjects.exists('fieldEditor')); }); } @@ -768,10 +779,6 @@ export class SettingsPageObject extends FtrService { await this.testSubjects.click('editFieldFormat'); } - async clickCloseEditFieldFormatFlyout() { - await this.testSubjects.click('euiFlyoutCloseButton'); - } - async associateIndexPattern(oldIndexPatternId: string, newIndexPatternTitle: string) { await this.find.clickByCssSelector( `select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] > diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 15a4f6d992da4..98304808224c7 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -45,6 +45,8 @@ export const useFormFieldMock = (options?: Partial>): FieldHook type: 'type', value: ('mockedValue' as unknown) as T, isPristine: false, + isDirty: false, + isModified: false, isValidating: false, isValidated: false, isChangingValue: false, diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index d0755d05bdb5f..337234bb752f5 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -91,6 +91,8 @@ export const useFormFieldMock = (options?: Partial>): FieldHook type: 'type', value: ('mockedValue' as unknown) as T, isPristine: false, + isDirty: false, + isModified: false, isValidating: false, isValidated: false, isChangingValue: false, diff --git a/x-pack/plugins/transform/common/api_schemas/common.ts b/x-pack/plugins/transform/common/api_schemas/common.ts index b84dcd2a4b749..5354598dc2475 100644 --- a/x-pack/plugins/transform/common/api_schemas/common.ts +++ b/x-pack/plugins/transform/common/api_schemas/common.ts @@ -67,6 +67,7 @@ export const runtimeMappingsSchema = schema.maybe( schema.literal('date'), schema.literal('ip'), schema.literal('boolean'), + schema.literal('geo_point'), ]), script: schema.maybe( schema.oneOf([ diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/plugins/transform/common/shared_imports.ts index 38cfb6bc457f1..42e77938d9cec 100644 --- a/x-pack/plugins/transform/common/shared_imports.ts +++ b/x-pack/plugins/transform/common/shared_imports.ts @@ -12,3 +12,5 @@ export { patternValidator, ChartData, } from '../../ml/common'; + +export { RUNTIME_FIELD_TYPES } from '../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index 8b3b33fdde3ef..6e5a34fa6ef2b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -24,7 +24,7 @@ import { } from '../../../../../../../common/types/transform'; import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms'; -import { isPopulatedObject } from '../../../../../../../common/shared_imports'; +import { isPopulatedObject, RUNTIME_FIELD_TYPES } from '../../../../../../../common/shared_imports'; export interface ErrorMessage { query: string; @@ -36,8 +36,6 @@ export interface Field { type: KBN_FIELD_TYPES; } -// Replace this with import once #88995 is merged -const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; export interface RuntimeField { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9d880276312f7..7bf0607e27bd5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2810,7 +2810,6 @@ "indexPatternFieldEditor.duration.showSuffixLabel": "接尾辞を表示", "indexPatternFieldEditor.duration.showSuffixLabel.short": "短縮サフィックスを使用", "indexPatternFieldEditor.durationErrorMessage": "小数部分の桁数は0から20までの間で指定する必要があります", - "indexPatternFieldEditor.editor.flyoutCloseButtonLabel": "閉じる", "indexPatternFieldEditor.editor.flyoutDefaultTitle": "フィールドを作成", "indexPatternFieldEditor.editor.flyoutEditFieldSubtitle": "インデックスパターン:{patternName}", "indexPatternFieldEditor.editor.flyoutEditFieldTitle": "「{fieldName}」フィールドの編集", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index addbe289ee990..fb662890571a5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2822,7 +2822,6 @@ "indexPatternFieldEditor.duration.showSuffixLabel": "显示后缀", "indexPatternFieldEditor.duration.showSuffixLabel.short": "使用短后缀", "indexPatternFieldEditor.durationErrorMessage": "小数位数必须介于 0 和 20 之间", - "indexPatternFieldEditor.editor.flyoutCloseButtonLabel": "关闭", "indexPatternFieldEditor.editor.flyoutDefaultTitle": "创建字段", "indexPatternFieldEditor.editor.flyoutEditFieldSubtitle": "索引模式:{patternName}", "indexPatternFieldEditor.editor.flyoutEditFieldTitle": "编辑字段“{fieldName}”", From 1311fe38ae7c5631b55cc1aacac7d2d274b734b3 Mon Sep 17 00:00:00 2001 From: Ross Bell Date: Fri, 13 Aug 2021 17:41:17 -0500 Subject: [PATCH 74/91] Add Workplace Search sync controls UI (#108558) * Wip * Things are more broken, but closer to the end goal * Get patch request working * Update event type * Other two toggles * Force sync button * Remove force sync button for now * Disable the checkbox when globally disabled and introduce click to save * Wip tests * One test down * Test for skipping name alert * Linter * Fix undefined check * Prettier * Apply suggestions from code review Co-authored-by: Scotty Bollinger * Refactor some structures into interfaces * UI tweaks Co-authored-by: Scotty Bollinger --- .../__mocks__/content_sources.mock.ts | 26 +++++- .../applications/workplace_search/types.ts | 15 +++ .../components/source_settings.test.tsx | 78 ++++++++++++++++ .../components/source_settings.tsx | 93 ++++++++++++++++++- .../views/content_sources/constants.ts | 42 +++++++++ .../content_sources/source_logic.test.ts | 15 +++ .../views/content_sources/source_logic.ts | 24 ++++- .../server/routes/workplace_search/sources.ts | 37 ++++++-- 8 files changed, 314 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index c599a13cc3119..5f515fc99769c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -28,7 +28,7 @@ export const contentSources = [ }, { id: '124', - serviceType: 'jira', + serviceType: 'jira_cloud', searchable: true, supportedByLicense: true, status: 'synced', @@ -43,6 +43,24 @@ export const contentSources = [ }, ]; +const defaultIndexing = { + enabled: true, + defaultAction: 'include', + rules: [], + schedule: { + intervals: [], + blocked: [], + }, + features: { + contentExtraction: { + enabled: true, + }, + thumbnails: { + enabled: true, + }, + }, +}; + export const fullContentSources = [ { ...contentSources[0], @@ -66,8 +84,11 @@ export const fullContentSources = [ type: 'summary', }, ], + indexing: defaultIndexing, groups, custom: false, + isIndexedSource: true, + areThumbnailsConfigEnabled: true, accessToken: '123token', urlField: 'myLink', titleField: 'heading', @@ -85,7 +106,10 @@ export const fullContentSources = [ details: [], summary: [], groups: [], + indexing: defaultIndexing, custom: true, + isIndexedSource: true, + areThumbnailsConfigEnabled: true, accessToken: '123token', urlField: 'url', titleField: 'title', 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 e50b12f781947..8f9528d52195e 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 @@ -129,12 +129,27 @@ interface SourceActivity { status: string; } +interface IndexingConfig { + enabled: boolean; + features: { + contentExtraction: { + enabled: boolean; + }; + thumbnails: { + enabled: boolean; + }; + }; +} + export interface ContentSourceFullData extends ContentSourceDetails { activities: SourceActivity[]; details: DescriptionList[]; summary: DocumentSummaryItem[]; groups: Group[]; + indexing: IndexingConfig; custom: boolean; + isIndexedSource: boolean; + areThumbnailsConfigEnabled: boolean; accessToken: string; urlField: string; titleField: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx index da4346d54727c..0276e75e4d219 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx @@ -105,6 +105,84 @@ describe('SourceSettings', () => { ); }); + it('handles disabling synchronization', () => { + const wrapper = shallow(); + + const synchronizeSwitch = wrapper.find('[data-test-subj="SynchronizeToggle"]').first(); + const event = { target: { checked: false } }; + synchronizeSwitch.prop('onChange')?.(event as any); + + wrapper.find('[data-test-subj="SaveSyncControlsButton"]').simulate('click'); + + expect(updateContentSource).toHaveBeenCalledWith(fullContentSources[0].id, { + indexing: { + enabled: false, + features: { + content_extraction: { enabled: true }, + thumbnails: { enabled: true }, + }, + }, + }); + }); + + it('handles disabling thumbnails', () => { + const wrapper = shallow(); + + const thumbnailsSwitch = wrapper.find('[data-test-subj="ThumbnailsToggle"]').first(); + const event = { target: { checked: false } }; + thumbnailsSwitch.prop('onChange')?.(event as any); + + wrapper.find('[data-test-subj="SaveSyncControlsButton"]').simulate('click'); + + expect(updateContentSource).toHaveBeenCalledWith(fullContentSources[0].id, { + indexing: { + enabled: true, + features: { + content_extraction: { enabled: true }, + thumbnails: { enabled: false }, + }, + }, + }); + }); + + it('handles disabling content extraction', () => { + const wrapper = shallow(); + + const contentExtractionSwitch = wrapper + .find('[data-test-subj="ContentExtractionToggle"]') + .first(); + const event = { target: { checked: false } }; + contentExtractionSwitch.prop('onChange')?.(event as any); + + wrapper.find('[data-test-subj="SaveSyncControlsButton"]').simulate('click'); + + expect(updateContentSource).toHaveBeenCalledWith(fullContentSources[0].id, { + indexing: { + enabled: true, + features: { + content_extraction: { enabled: false }, + thumbnails: { enabled: true }, + }, + }, + }); + }); + + it('disables the thumbnails switch when globally disabled', () => { + setMockValues({ + ...mockValues, + contentSource: { + ...fullContentSources[0], + areThumbnailsConfigEnabled: false, + }, + }); + + const wrapper = shallow(); + + const synchronizeSwitch = wrapper.find('[data-test-subj="ThumbnailsToggle"]'); + + expect(synchronizeSwitch.prop('disabled')).toEqual(true); + }); + describe('DownloadDiagnosticsButton', () => { it('renders for org with correct href', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index e4f52d94ad9e7..d6f16db4d5129 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -17,6 +17,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiSpacer, + EuiSwitch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -50,6 +52,12 @@ import { SYNC_DIAGNOSTICS_TITLE, SYNC_DIAGNOSTICS_DESCRIPTION, SYNC_DIAGNOSTICS_BUTTON, + SYNC_MANAGEMENT_TITLE, + SYNC_MANAGEMENT_DESCRIPTION, + SYNC_MANAGEMENT_SYNCHRONIZE_LABEL, + SYNC_MANAGEMENT_THUMBNAILS_LABEL, + SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL, + SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL, } from '../constants'; import { staticSourceData } from '../source_data'; import { SourceLogic } from '../source_logic'; @@ -63,7 +71,21 @@ export const SourceSettings: React.FC = () => { const { getSourceConfigData } = useActions(AddSourceLogic); const { - contentSource: { name, id, serviceType }, + contentSource: { + name, + id, + serviceType, + custom: isCustom, + isIndexedSource, + areThumbnailsConfigEnabled, + indexing: { + enabled, + features: { + contentExtraction: { enabled: contentExtractionEnabled }, + thumbnails: { enabled: thumbnailsEnabled }, + }, + }, + }, buttonLoading, } = useValues(SourceLogic); @@ -88,6 +110,11 @@ export const SourceSettings: React.FC = () => { const hideConfirm = () => setModalVisibility(false); const showConfig = isOrganization && !isEmpty(configuredFields); + const showSyncControls = isOrganization && isIndexedSource && !isCustom; + + const [synchronizeChecked, setSynchronize] = useState(enabled); + const [thumbnailsChecked, setThumbnails] = useState(thumbnailsEnabled); + const [contentExtractionChecked, setContentExtraction] = useState(contentExtractionEnabled); const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; @@ -102,6 +129,18 @@ export const SourceSettings: React.FC = () => { updateContentSource(id, { name: inputValue }); }; + const submitSyncControls = () => { + updateContentSource(id, { + indexing: { + enabled: synchronizeChecked, + features: { + content_extraction: { enabled: contentExtractionChecked }, + thumbnails: { enabled: thumbnailsChecked }, + }, + }, + }); + }; + const handleSourceRemoval = () => { /** * The modal was just hanging while the UI waited for the server to respond. @@ -180,6 +219,58 @@ export const SourceSettings: React.FC = () => { )} + {showSyncControls && ( + + + + setSynchronize(e.target.checked)} + label={SYNC_MANAGEMENT_SYNCHRONIZE_LABEL} + data-test-subj="SynchronizeToggle" + /> + + + + + + setThumbnails(e.target.checked)} + label={ + areThumbnailsConfigEnabled + ? SYNC_MANAGEMENT_THUMBNAILS_LABEL + : SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL + } + disabled={!areThumbnailsConfigEnabled} + data-test-subj="ThumbnailsToggle" + /> + + + + + setContentExtraction(e.target.checked)} + label={SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL} + data-test-subj="ContentExtractionToggle" + /> + + + + + + + {SAVE_CHANGES_BUTTON} + + + + + )} { expect(onUpdateSourceNameSpy).toHaveBeenCalledWith(contentSource.name); }); + it('does not call onUpdateSourceName when the name is not supplied', async () => { + AppLogic.values.isOrganization = true; + + const onUpdateSourceNameSpy = jest.spyOn(SourceLogic.actions, 'onUpdateSourceName'); + const promise = Promise.resolve(contentSource); + http.patch.mockReturnValue(promise); + SourceLogic.actions.updateContentSource(contentSource.id, { indexing: { enabled: true } }); + + expect(http.patch).toHaveBeenCalledWith('/api/workplace_search/org/sources/123/settings', { + body: JSON.stringify({ content_source: { indexing: { enabled: true } } }), + }); + await promise; + expect(onUpdateSourceNameSpy).not.toHaveBeenCalledWith(contentSource.name); + }); + it('calls API and sets values (account)', async () => { AppLogic.values.isOrganization = false; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 0fd44e01ae495..4d145bf798160 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -34,8 +34,11 @@ export interface SourceActions { searchContentSourceDocuments(sourceId: string): { sourceId: string }; updateContentSource( sourceId: string, - source: { name: string } - ): { sourceId: string; source: { name: string } }; + source: SourceUpdatePayload + ): { + sourceId: string; + source: ContentSourceFullData; + }; resetSourceState(): void; removeContentSource(sourceId: string): { sourceId: string }; initializeSource(sourceId: string): { sourceId: string }; @@ -57,6 +60,17 @@ interface SearchResultsResponse { meta: Meta; } +interface SourceUpdatePayload { + name?: string; + indexing?: { + enabled?: boolean; + features?: { + thumbnails?: { enabled: boolean }; + content_extraction?: { enabled: boolean }; + }; + }; +} + export const SourceLogic = kea>({ path: ['enterprise_search', 'workplace_search', 'source_logic'], actions: { @@ -69,7 +83,7 @@ export const SourceLogic = kea>({ initializeSource: (sourceId: string) => ({ sourceId }), initializeFederatedSummary: (sourceId: string) => ({ sourceId }), searchContentSourceDocuments: (sourceId: string) => ({ sourceId }), - updateContentSource: (sourceId: string, source: { name: string }) => ({ sourceId, source }), + updateContentSource: (sourceId: string, source: SourceUpdatePayload) => ({ sourceId, source }), removeContentSource: (sourceId: string) => ({ sourceId, }), @@ -209,7 +223,9 @@ export const SourceLogic = kea>({ const response = await HttpLogic.values.http.patch(route, { body: JSON.stringify({ content_source: source }), }); - actions.onUpdateSourceName(response.name); + if (source.name) { + actions.onUpdateSourceName(response.name); + } } catch (e) { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 835ad84ef6853..cb56f54a1df6a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -57,6 +57,31 @@ const displaySettingsSchema = schema.object({ detailFields: schema.oneOf([schema.arrayOf(displayFieldSchema), displayFieldSchema]), }); +const sourceSettingsSchema = schema.object({ + content_source: schema.object({ + name: schema.maybe(schema.string()), + indexing: schema.maybe( + schema.object({ + enabled: schema.maybe(schema.boolean()), + features: schema.maybe( + schema.object({ + thumbnails: schema.maybe( + schema.object({ + enabled: schema.boolean(), + }) + ), + content_extraction: schema.maybe( + schema.object({ + enabled: schema.boolean(), + }) + ), + }) + ), + }) + ), + }), +}); + // Account routes export function registerAccountSourcesRoute({ router, @@ -217,11 +242,7 @@ export function registerAccountSourceSettingsRoute({ { path: '/api/workplace_search/account/sources/{id}/settings', validate: { - body: schema.object({ - content_source: schema.object({ - name: schema.string(), - }), - }), + body: sourceSettingsSchema, params: schema.object({ id: schema.string(), }), @@ -565,11 +586,7 @@ export function registerOrgSourceSettingsRoute({ { path: '/api/workplace_search/org/sources/{id}/settings', validate: { - body: schema.object({ - content_source: schema.object({ - name: schema.string(), - }), - }), + body: sourceSettingsSchema, params: schema.object({ id: schema.string(), }), From 5ef1f95711377434d14688508d47b4f005b47348 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Fri, 13 Aug 2021 15:51:23 -0700 Subject: [PATCH 75/91] Add signal.original_event.reason to signal_extra_fields for insertion into old indices (#108594) --- .../routes/index/signal_extra_fields.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json index 7bc20fd540b9b..32c084f927d7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json @@ -43,6 +43,14 @@ } } }, + "original_event": { + "type": "object", + "properties": { + "reason": { + "type": "keyword" + } + } + }, "reason": { "type": "keyword" }, From bfea4a1c2bab0df1ff56df2f48ddfeda4b15e408 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Fri, 13 Aug 2021 16:49:55 -0700 Subject: [PATCH 76/91] Add EuiCodeEditor to ES UI Shared. (#108318) * Export EuiCodeEditor from es_ui_shared and consume it in Grok Debugger. Remove warning from EuiCodeEditor. * Lazy-load code editor so it doesn't bloat the EsUiShared plugin bundle. * Refactor mocks into a shared jest_mock.tsx file. --- package.json | 2 +- .../__snapshots__/code_editor.test.tsx.snap | 627 ++++++++++++++++++ .../components/code_editor/_code_editor.scss | 38 ++ .../public/components/code_editor/_index.scss | 1 + .../code_editor/code_editor.test.tsx | 117 ++++ .../components/code_editor/code_editor.tsx | 308 +++++++++ .../public/components/code_editor/index.tsx | 32 + .../components/code_editor/jest_mock.tsx | 32 + .../components/json_editor/json_editor.tsx | 3 +- src/plugins/es_ui_shared/public/index.ts | 1 + x-pack/plugins/grokdebugger/kibana.json | 3 +- .../custom_patterns_input.js | 13 +- .../components/event_input/event_input.js | 6 +- .../components/event_output/event_output.js | 4 +- .../components/pattern_input/pattern_input.js | 6 +- .../grokdebugger/public/shared_imports.ts | 8 + .../component_template_create.test.tsx | 5 +- .../helpers/mappings_editor.helpers.tsx | 2 +- .../helpers/setup_environment.tsx | 12 +- .../load_mappings_provider.test.tsx | 18 +- .../__jest__/test_pipeline.helpers.tsx | 22 +- .../load_from_json/modal_provider.test.tsx | 18 +- yarn.lock | 14 +- 23 files changed, 1199 insertions(+), 93 deletions(-) create mode 100644 src/plugins/es_ui_shared/public/components/code_editor/__snapshots__/code_editor.test.tsx.snap create mode 100644 src/plugins/es_ui_shared/public/components/code_editor/_code_editor.scss create mode 100644 src/plugins/es_ui_shared/public/components/code_editor/_index.scss create mode 100644 src/plugins/es_ui_shared/public/components/code_editor/code_editor.test.tsx create mode 100644 src/plugins/es_ui_shared/public/components/code_editor/code_editor.tsx create mode 100644 src/plugins/es_ui_shared/public/components/code_editor/index.tsx create mode 100644 src/plugins/es_ui_shared/public/components/code_editor/jest_mock.tsx create mode 100644 x-pack/plugins/grokdebugger/public/shared_imports.ts diff --git a/package.json b/package.json index 17beadebca91c..db8f9fac9238f 100644 --- a/package.json +++ b/package.json @@ -336,7 +336,7 @@ "re-resizable": "^6.1.1", "re2": "^1.15.4", "react": "^16.12.0", - "react-ace": "^5.9.0", + "react-ace": "^7.0.5", "react-beautiful-dnd": "^13.0.0", "react-color": "^2.13.8", "react-dom": "^16.12.0", diff --git a/src/plugins/es_ui_shared/public/components/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/es_ui_shared/public/components/code_editor/__snapshots__/code_editor.test.tsx.snap new file mode 100644 index 0000000000000..aeab9a66c7694 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/code_editor/__snapshots__/code_editor.test.tsx.snap @@ -0,0 +1,627 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiCodeEditor behavior hint element should be disabled when the ui ace box gains focus 1`] = ` + +`; + +exports[`EuiCodeEditor behavior hint element should be enabled when the ui ace box loses focus 1`] = ` + +`; + +exports[`EuiCodeEditor behavior hint element should be tabable 1`] = ` + +`; + +exports[`EuiCodeEditor is rendered 1`] = ` +
+ +
+