From b23a139e888f1a944f1d235dc15f25839a884e60 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 8 Nov 2021 11:50:39 -0500 Subject: [PATCH 01/98] [Observability] [Exploratory View] add exploratory view telemetry (#115882) * add exploratory view telemetry * add chart load time telemetry * adjust types * update telemetry logic * adjust types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../hooks/use_series_storage.test.tsx | 114 ++++++++++++----- .../hooks/use_series_storage.tsx | 9 +- .../exploratory_view/lens_embeddable.tsx | 30 +++-- .../exploratory_view/utils/telemetry.test.tsx | 120 ++++++++++++++++++ .../exploratory_view/utils/telemetry.ts | 110 ++++++++++++++++ .../public/hooks/use_track_metric.tsx | 3 +- 6 files changed, 346 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.ts diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index ffe7db0568344..1d23796b5bf55 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -12,6 +12,7 @@ import { render } from '@testing-library/react'; import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; import { getHistoryFromUrl } from '../rtl_helpers'; import type { AppDataType } from '../types'; +import * as useTrackMetric from '../../../../hooks/use_track_metric'; const mockSingleSeries = [ { @@ -28,12 +29,26 @@ const mockMultipleSeries = [ dataType: 'ux' as AppDataType, breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, + filters: [ + { + field: 'url.full', + value: 'https://elastic.co', + }, + ], + selectedMetricField: 'transaction.duration.us', }, { name: 'kpi-over-time', dataType: 'synthetics' as AppDataType, breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, + filters: [ + { + field: 'monitor.type', + value: 'browser', + }, + ], + selectedMetricField: 'monitor.duration.us', }, ]; @@ -100,26 +115,8 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - }, - { - name: 'kpi-over-time', - dataType: 'synthetics', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - }, - ], - firstSeries: { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - }, + allSeries: mockMultipleSeries, + firstSeries: mockMultipleSeries[0], }) ); }); @@ -158,18 +155,77 @@ describe('userSeriesStorage', function () { }); expect(result.current.allSeries).toEqual([ + mockMultipleSeries[0], { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - }, - { - name: 'kpi-over-time', - dataType: 'synthetics', + ...mockMultipleSeries[1], breakdown: undefined, - time: { from: 'now-15m', to: 'now' }, }, ]); }); + + it('ensures that telemetry is called', () => { + const trackEvent = jest.fn(); + jest.spyOn(useTrackMetric, 'useUiTracker').mockReturnValue(trackEvent); + function wrapper({ children }: { children: React.ReactElement }) { + return ( + + key === 'sr' ? mockMultipleSeries : 'kpi-over-time' + ), + set: jest.fn(), + }} + > + {children} + + ); + } + const { result } = renderHook(() => useSeriesStorage(), { wrapper }); + + act(() => { + result.current.applyChanges(); + }); + + expect(trackEvent).toBeCalledTimes(7); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__filter_url.full', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__filter_monitor.type', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__report_type_kpi-over-time__data_type_ux__filter_url.full', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__filters__report_type_kpi-over-time__data_type_synthetics__filter_monitor.type', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__report_type_kpi-over-time__data_type_synthetics__metric_type_monitor.duration.us', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__report_type_kpi-over-time__data_type_ux__metric_type_transaction.duration.us', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view_apply_changes', + metricType: 'count', + }); + }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index e71c66ba1f11b..2e8369bd1ddd4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -10,6 +10,7 @@ import { IKbnUrlStateStorage, ISessionStorageStateStorage, } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { useUiTracker } from '../../../../hooks/use_track_metric'; import type { AppDataType, ReportViewType, @@ -20,6 +21,7 @@ import type { import { convertToShortUrl } from '../configurations/utils'; import { OperationType, SeriesType } from '../../../../../../lens/public'; import { URL_KEYS } from '../configurations/constants/url_constants'; +import { trackTelemetryOnApply } from '../utils/telemetry'; export interface SeriesContextValue { firstSeries?: SeriesUrl; @@ -63,6 +65,8 @@ export function UrlStorageContextProvider({ const [firstSeries, setFirstSeries] = useState(); + const trackEvent = useUiTracker(); + useEffect(() => { const firstSeriesT = allSeries?.[0]; @@ -116,11 +120,14 @@ export function UrlStorageContextProvider({ (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); setLastRefresh(Date.now()); + + trackTelemetryOnApply(trackEvent, allSeries, reportType); + if (onApply) { onApply(); } }, - [allSeries, storage] + [allSeries, storage, trackEvent, reportType] ); const value = { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index b9dced8036eae..437981baf81d5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -9,11 +9,13 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; import styled from 'styled-components'; import { TypedLensByValueInput } from '../../../../../lens/public'; +import { useUiTracker } from '../../../hooks/use_track_metric'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useExpViewTimeRange } from './hooks/use_time_range'; import { parseRelativeDate } from './components/date_range_picker'; +import { trackTelemetryOnLoad } from './utils/telemetry'; import type { ChartTimeRange } from './header/last_updated'; interface Props { @@ -23,26 +25,36 @@ interface Props { export function LensEmbeddable(props: Props) { const { lensAttributes, setChartTimeRangeContext } = props; - const { services: { lens, notifications }, } = useKibana(); const LensComponent = lens?.EmbeddableComponent; - const { firstSeries, setSeries, reportType } = useSeriesStorage(); + const { firstSeries, setSeries, reportType, lastRefresh } = useSeriesStorage(); const firstSeriesId = 0; const timeRange = useExpViewTimeRange(); - const onLensLoad = useCallback(() => { - setChartTimeRangeContext({ - lastUpdated: Date.now(), - to: parseRelativeDate(timeRange?.to || '').valueOf(), - from: parseRelativeDate(timeRange?.from || '').valueOf(), - }); - }, [setChartTimeRangeContext, timeRange]); + const trackEvent = useUiTracker(); + + const onLensLoad = useCallback( + (isLoading) => { + const timeLoaded = Date.now(); + + setChartTimeRangeContext({ + lastUpdated: timeLoaded, + to: parseRelativeDate(timeRange?.to || '').valueOf(), + from: parseRelativeDate(timeRange?.from || '').valueOf(), + }); + + if (!isLoading) { + trackTelemetryOnLoad(trackEvent, lastRefresh, timeLoaded); + } + }, + [setChartTimeRangeContext, timeRange, lastRefresh, trackEvent] + ); const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.test.tsx new file mode 100644 index 0000000000000..cf24cd47d9d10 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { AppDataType } from '../types'; +import { trackTelemetryOnApply, trackTelemetryOnLoad } from './telemetry'; + +const mockMultipleSeries = [ + { + name: 'performance-distribution', + dataType: 'ux' as AppDataType, + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + filters: [ + { + field: 'url.full', + value: 'https://elastic.co', + }, + ], + selectedMetricField: 'transaction.duration.us', + }, + { + name: 'kpi-over-time', + dataType: 'synthetics' as AppDataType, + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + filters: [ + { + field: 'monitor.type', + value: 'browser', + }, + ], + selectedMetricField: 'monitor.duration.us', + }, +]; + +describe('telemetry', function () { + it('ensures that appropriate telemetry is called when settings are applied', () => { + const trackEvent = jest.fn(); + trackTelemetryOnApply(trackEvent, mockMultipleSeries, 'kpi-over-time'); + + expect(trackEvent).toBeCalledTimes(7); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__filter_url.full', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__filter_monitor.type', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view__filters__report_type_kpi-over-time__data_type_ux__filter_url.full', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__filters__report_type_kpi-over-time__data_type_synthetics__filter_monitor.type', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__report_type_kpi-over-time__data_type_synthetics__metric_type_monitor.duration.us', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: + 'exploratory_view__report_type_kpi-over-time__data_type_ux__metric_type_transaction.duration.us', + metricType: 'count', + }); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view_apply_changes', + metricType: 'count', + }); + }); + + it('does not call track event for report type/data type/metric type config unless all values are truthy', () => { + const trackEvent = jest.fn(); + const series = { + ...mockMultipleSeries[1], + filters: undefined, + selectedMetricField: undefined, + }; + + trackTelemetryOnApply(trackEvent, [series], 'kpi-over-time'); + + expect(trackEvent).toBeCalledTimes(1); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: 'exploratory_view_apply_changes', + metricType: 'count', + }); + }); + + it.each([ + [1635784025000, '5-10'], + [1635784030000, '10-20'], + [1635784040000, '20-30'], + [1635784050000, '30-60'], + [1635784080000, '60+'], + ])('ensures that appropriate telemetry is called when chart is loaded', (endTime, range) => { + const trackEvent = jest.fn(); + trackTelemetryOnLoad(trackEvent, 1635784020000, endTime); + + expect(trackEvent).toBeCalledTimes(1); + expect(trackEvent).toBeCalledWith({ + app: 'observability-overview', + metric: `exploratory_view__chart_loading_in_seconds_${range}`, + metricType: 'count', + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.ts new file mode 100644 index 0000000000000..76d99824c26f0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/telemetry.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TrackEvent, METRIC_TYPE } from '../../../../hooks/use_track_metric'; +import type { SeriesUrl } from '../types'; + +export const trackTelemetryOnApply = ( + trackEvent: TrackEvent, + allSeries: SeriesUrl[], + reportType: string +) => { + trackFilters(trackEvent, allSeries, reportType); + trackDataType(trackEvent, allSeries, reportType); + trackApplyChanges(trackEvent); +}; + +export const trackTelemetryOnLoad = (trackEvent: TrackEvent, start: number, end: number) => { + trackChartLoadingTime(trackEvent, start, end); +}; + +const getAppliedFilters = (allSeries: SeriesUrl[]) => { + const filtersByDataType = new Map(); + allSeries.forEach((series) => { + const seriesFilters = filtersByDataType.get(series.dataType); + const filterFields = (series.filters || []).map((filter) => filter.field); + + if (seriesFilters) { + seriesFilters.push(...filterFields); + } else { + filtersByDataType.set(series.dataType, [...filterFields]); + } + }); + return filtersByDataType; +}; + +const trackFilters = (trackEvent: TrackEvent, allSeries: SeriesUrl[], reportType: string) => { + const filtersByDataType = getAppliedFilters(allSeries); + [...filtersByDataType.keys()].forEach((dataType) => { + const filtersForDataType = filtersByDataType.get(dataType); + + (filtersForDataType || []).forEach((filter) => { + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: `exploratory_view__filters__filter_${filter}`, + }); + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: `exploratory_view__filters__report_type_${reportType}__data_type_${dataType}__filter_${filter}`, + }); + }); + }); +}; + +const trackApplyChanges = (trackEvent: TrackEvent) => { + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: 'exploratory_view_apply_changes', + }); +}; + +const trackDataType = (trackEvent: TrackEvent, allSeries: SeriesUrl[], reportType: string) => { + const metrics = allSeries.map((series) => ({ + dataType: series.dataType, + metricType: series.selectedMetricField, + })); + + metrics.forEach(({ dataType, metricType }) => { + if (reportType && dataType && metricType) { + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: `exploratory_view__report_type_${reportType}__data_type_${dataType}__metric_type_${metricType}`, + }); + } + }); +}; + +export const trackChartLoadingTime = (trackEvent: TrackEvent, start: number, end: number) => { + const secondsLoading = (end - start) / 1000; + const rangeStr = toRangeStr(secondsLoading); + + if (rangeStr) { + trackChartLoadingMetric(trackEvent, rangeStr); + } +}; + +function toRangeStr(n: number) { + if (n < 0 || isNaN(n)) return null; + if (n >= 60) return '60+'; + else if (n >= 30) return '30-60'; + else if (n >= 20) return '20-30'; + else if (n >= 10) return '10-20'; + else if (n >= 5) return '5-10'; + return '0-5'; +} + +const trackChartLoadingMetric = (trackEvent: TrackEvent, range: string) => { + trackEvent({ + app: 'observability-overview', + metricType: METRIC_TYPE.COUNT, + metric: `exploratory_view__chart_loading_in_seconds_${range}`, + }); +}; diff --git a/x-pack/plugins/observability/public/hooks/use_track_metric.tsx b/x-pack/plugins/observability/public/hooks/use_track_metric.tsx index 1410389fc3bac..b2327b590156a 100644 --- a/x-pack/plugins/observability/public/hooks/use_track_metric.tsx +++ b/x-pack/plugins/observability/public/hooks/use_track_metric.tsx @@ -32,12 +32,13 @@ interface ServiceDeps { export type TrackMetricOptions = TrackOptions & { metric: string }; export type UiTracker = ReturnType; +export type TrackEvent = (options: TrackMetricOptions) => void; export { METRIC_TYPE }; export function useUiTracker({ app: defaultApp, -}: { app?: ObservabilityApp } = {}) { +}: { app?: ObservabilityApp } = {}): TrackEvent { const reportUiCounter = useKibana().services?.usageCollection?.reportUiCounter; const trackEvent = useMemo(() => { return ({ app = defaultApp, metric, metricType = METRIC_TYPE.COUNT }: TrackMetricOptions) => { From 0c60cccff2749e991319928d3230032362df71b6 Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Mon, 8 Nov 2021 09:52:49 -0700 Subject: [PATCH 02/98] [Lens] fix focus on legend action popovers (#115066) --- .../pie/public/utils/get_legend_actions.tsx | 9 +++++++-- .../xy/public/utils/get_legend_actions.tsx | 14 ++++++++++++-- .../shared_components/legend_action_popover.tsx | 8 +++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx b/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx index cd1d1d71aaa76..2a27063a0e7d5 100644 --- a/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx +++ b/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx @@ -10,7 +10,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; -import { LegendAction, SeriesIdentifier } from '@elastic/charts'; +import { LegendAction, SeriesIdentifier, useLegendAction } from '@elastic/charts'; import { DataPublicPluginStart } from '../../../../data/public'; import { PieVisParams } from '../types'; import { ClickTriggerEvent } from '../../../../charts/public'; @@ -30,6 +30,7 @@ export const getLegendActions = ( const [popoverOpen, setPopoverOpen] = useState(false); const [isfilterable, setIsfilterable] = useState(true); const filterData = useMemo(() => getFilterEventData(pieSeries), [pieSeries]); + const [ref, onClose] = useLegendAction(); useEffect(() => { (async () => setIsfilterable(await canFilter(filterData, actions)))(); @@ -82,6 +83,7 @@ export const getLegendActions = ( const Button = (
setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('visTypePie.legend.filterOptionsLegend', { diff --git a/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx b/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx index 98ace7dd57a39..d52e3a457f8e9 100644 --- a/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx +++ b/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx @@ -10,7 +10,12 @@ import React, { useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; -import { LegendAction, XYChartSeriesIdentifier, SeriesName } from '@elastic/charts'; +import { + LegendAction, + XYChartSeriesIdentifier, + SeriesName, + useLegendAction, +} from '@elastic/charts'; import { ClickTriggerEvent } from '../../../../charts/public'; @@ -25,6 +30,7 @@ export const getLegendActions = ( const [isfilterable, setIsfilterable] = useState(false); const series = xySeries as XYChartSeriesIdentifier; const filterData = useMemo(() => getFilterEventData(series), [series]); + const [ref, onClose] = useLegendAction(); useEffect(() => { (async () => setIsfilterable(await canFilter(filterData)))(); @@ -69,6 +75,7 @@ export const getLegendActions = ( const Button = (
setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('visTypeXy.legend.filterOptionsLegend', { diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx index 5027629ef6ae5..fa7e12083435c 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx @@ -8,6 +8,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import { useLegendAction } from '@elastic/charts'; import type { LensFilterEvent } from '../types'; export interface LegendActionPopoverProps { @@ -31,6 +32,7 @@ export const LegendActionPopover: React.FunctionComponent { const [popoverOpen, setPopoverOpen] = useState(false); + const [ref, onClose] = useLegendAction(); const panels: EuiContextMenuPanelDescriptor[] = [ { id: 'main', @@ -65,6 +67,7 @@ export const LegendActionPopover: React.FunctionComponent setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('xpack.lens.shared.legend.filterOptionsLegend', { From 828a5790e4b16ad13bd5cf2367e2082a24ad3c21 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 8 Nov 2021 18:58:31 +0200 Subject: [PATCH 03/98] [Lens] Unskips heatmap test suite & stabilizes the dimensionConfiguration helper (#117811) * [Lens] Unskips heatmap test suite * Check if operation button exists --- x-pack/test/functional/apps/lens/heatmap.ts | 13 +++++++------ x-pack/test/functional/page_objects/lens_page.ts | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/heatmap.ts index 5b80e6ad5cf55..3f8456e9d75f6 100644 --- a/x-pack/test/functional/apps/lens/heatmap.ts +++ b/x-pack/test/functional/apps/lens/heatmap.ts @@ -12,10 +12,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); const elasticChart = getService('elasticChart'); const testSubjects = getService('testSubjects'); + const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/117404 - // FLAKY: https://github.com/elastic/kibana/issues/113043 - describe.skip('lens heatmap', () => { + describe('lens heatmap', () => { before(async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); @@ -73,9 +72,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should reflect stop colors change on the chart', async () => { await PageObjects.lens.openDimensionEditor('lnsHeatmap_cellPanel > lns-dimensionTrigger'); await PageObjects.lens.openPalettePanel('lnsHeatmap'); - await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '10', { - clearWithKeyboard: true, - typeCharByChar: true, + await retry.try(async () => { + await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '10', { + clearWithKeyboard: true, + typeCharByChar: true, + }); }); await PageObjects.lens.waitForVisualization(); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 266aa4955b6e8..247dc607c0038 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -129,9 +129,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const operationSelector = opts.isPreviousIncompatible ? `lns-indexPatternDimension-${opts.operation} incompatible` : `lns-indexPatternDimension-${opts.operation}`; - await testSubjects.click(operationSelector); + await retry.try(async () => { + await testSubjects.exists(operationSelector); + await testSubjects.click(operationSelector); + }); } - if (opts.field) { const target = await testSubjects.find('indexPattern-dimension-field'); await comboBox.openOptionsList(target); From 842852761ecf027ac16695eca4b57500537de766 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 8 Nov 2021 10:14:32 -0700 Subject: [PATCH 04/98] [Reporting] Increase functional tests on feature priviliges (#117443) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_roles_privileges.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts b/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts index ec526ac1cd272..1ad7379aa184e 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/security_roles_privileges.ts @@ -7,12 +7,12 @@ import expect from '@kbn/expect'; import { SearchSourceFields } from 'src/plugins/data/common'; -import supertest from 'supertest'; import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); + const supertest = getService('supertest'); describe('Security Roles and Privileges for Applications', () => { before(async () => { @@ -25,7 +25,7 @@ export default function ({ getService }: FtrProviderContext) { describe('Dashboard: CSV download file', () => { it('does not allow user that does not have the role-based privilege', async () => { - const res = (await reportingAPI.downloadCsv( + const res = await reportingAPI.downloadCsv( reportingAPI.DATA_ANALYST_USERNAME, reportingAPI.DATA_ANALYST_PASSWORD, { @@ -37,12 +37,12 @@ export default function ({ getService }: FtrProviderContext) { browserTimezone: 'UTC', title: 'testfooyu78yt90-', } - )) as supertest.Response; + ); expect(res.status).to.eql(403); }); it('does allow user with the role privilege', async () => { - const res = (await reportingAPI.downloadCsv( + const res = await reportingAPI.downloadCsv( reportingAPI.REPORTING_USER_USERNAME, reportingAPI.REPORTING_USER_PASSWORD, { @@ -54,7 +54,7 @@ export default function ({ getService }: FtrProviderContext) { browserTimezone: 'UTC', title: 'testfooyu78yt90-', } - )) as supertest.Response; + ); expect(res.status).to.eql(200); }); }); @@ -197,5 +197,21 @@ export default function ({ getService }: FtrProviderContext) { expect(res.status).to.eql(200); }); }); + + // This tests the same API as x-pack/test/api_integration/apis/security/privileges.ts, but it uses the non-deprecated config + it('should register reporting privileges with the security privileges API', async () => { + await supertest + .get('/api/security/privileges') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200) + .expect((res) => { + expect(res.body.features.canvas).match(/generate_report/); + expect(res.body.features.dashboard).match(/download_csv_report/); + expect(res.body.features.dashboard).match(/generate_report/); + expect(res.body.features.discover).match(/generate_report/); + expect(res.body.features.visualize).match(/generate_report/); + }); + }); }); } From 923df6557fb4bbbe673be674fb3b5f07dd43d284 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 8 Nov 2021 12:27:28 -0500 Subject: [PATCH 05/98] [SECURITY] Remove flakiness around edit user (#117558) * wip * convert flaky jest test to functional test * improvement from review * fix * fix i18n Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_user/change_password_flyout.test.tsx | 137 +++++++++ .../edit_user/change_password_flyout.tsx | 93 +++--- .../users/edit_user/edit_user_page.test.tsx | 274 +----------------- .../users/edit_user/edit_user_page.tsx | 25 +- .../management/users/edit_user/user_form.tsx | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - x-pack/test/functional/apps/security/users.ts | 128 +++++++- .../functional/page_objects/security_page.ts | 87 +++++- 9 files changed, 395 insertions(+), 352 deletions(-) create mode 100644 x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.test.tsx diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.test.tsx new file mode 100644 index 0000000000000..b0b8ca2030fa0 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 { ChangePasswordFormValues } from './change_password_flyout'; +import { validateChangePasswordForm } from './change_password_flyout'; + +describe('ChangePasswordFlyout', () => { + describe('#validateChangePasswordForm', () => { + describe('for current user', () => { + it('should show an error when it is current user with no current password', () => { + expect( + validateChangePasswordForm({ password: 'changeme', confirm_password: 'changeme' }, true) + ).toMatchInlineSnapshot(` + Object { + "current_password": "Enter your current password.", + } + `); + }); + + it('should show errors when there is no new password', () => { + expect( + validateChangePasswordForm( + { + password: undefined, + confirm_password: 'changeme', + } as unknown as ChangePasswordFormValues, + true + ) + ).toMatchInlineSnapshot(` + Object { + "current_password": "Enter your current password.", + "password": "Enter a new password.", + } + `); + }); + + it('should show errors when the new password is not at least 6 characters', () => { + expect(validateChangePasswordForm({ password: '12345', confirm_password: '12345' }, true)) + .toMatchInlineSnapshot(` + Object { + "current_password": "Enter your current password.", + "password": "Password must be at least 6 characters.", + } + `); + }); + + it('should show errors when new password does not match confirmation password', () => { + expect( + validateChangePasswordForm( + { + password: 'changeme', + confirm_password: 'notTheSame', + }, + true + ) + ).toMatchInlineSnapshot(` + Object { + "confirm_password": "Passwords do not match.", + "current_password": "Enter your current password.", + } + `); + }); + + it('should show NO errors', () => { + expect( + validateChangePasswordForm( + { + current_password: 'oldpassword', + password: 'changeme', + confirm_password: 'changeme', + }, + true + ) + ).toMatchInlineSnapshot(`Object {}`); + }); + }); + + describe('for another user', () => { + it('should show errors when there is no new password', () => { + expect( + validateChangePasswordForm( + { + password: undefined, + confirm_password: 'changeme', + } as unknown as ChangePasswordFormValues, + false + ) + ).toMatchInlineSnapshot(` + Object { + "password": "Enter a new password.", + } + `); + }); + + it('should show errors when the new password is not at least 6 characters', () => { + expect(validateChangePasswordForm({ password: '1234', confirm_password: '1234' }, false)) + .toMatchInlineSnapshot(` + Object { + "password": "Password must be at least 6 characters.", + } + `); + }); + + it('should show errors when new password does not match confirmation password', () => { + expect( + validateChangePasswordForm( + { + password: 'changeme', + confirm_password: 'notTheSame', + }, + false + ) + ).toMatchInlineSnapshot(` + Object { + "confirm_password": "Passwords do not match.", + } + `); + }); + + it('should show NO errors', () => { + expect( + validateChangePasswordForm( + { + password: 'changeme', + confirm_password: 'changeme', + }, + false + ) + ).toMatchInlineSnapshot(`Object {}`); + }); + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx index 445d424adb388..29282696ffdf1 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx @@ -44,6 +44,49 @@ export interface ChangePasswordFlyoutProps { onSuccess?(): void; } +export const validateChangePasswordForm = ( + values: ChangePasswordFormValues, + isCurrentUser: boolean +) => { + const errors: ValidationErrors = {}; + + if (isCurrentUser) { + if (!values.current_password) { + errors.current_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError', + { + defaultMessage: 'Enter your current password.', + } + ); + } + } + + if (!values.password) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordRequiredError', + { + defaultMessage: 'Enter a new password.', + } + ); + } else if (values.password.length < 6) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordInvalidError', + { + defaultMessage: 'Password must be at least 6 characters.', + } + ); + } else if (values.password !== values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } + + return errors; +}; + export const ChangePasswordFlyout: FunctionComponent = ({ username, defaultValues = { @@ -99,52 +142,7 @@ export const ChangePasswordFlyout: FunctionComponent } } }, - validate: async (values) => { - const errors: ValidationErrors = {}; - - if (isCurrentUser) { - if (!values.current_password) { - errors.current_password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError', - { - defaultMessage: 'Enter your current password.', - } - ); - } - } - - if (!values.password) { - errors.password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.passwordRequiredError', - { - defaultMessage: 'Enter a new password.', - } - ); - } else if (values.password.length < 6) { - errors.password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.passwordInvalidError', - { - defaultMessage: 'Password must be at least 6 characters.', - } - ); - } else if (!values.confirm_password) { - errors.confirm_password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.confirmPasswordRequiredError', - { - defaultMessage: 'Passwords do not match.', - } - ); - } else if (values.password !== values.confirm_password) { - errors.confirm_password = i18n.translate( - 'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError', - { - defaultMessage: 'Passwords do not match.', - } - ); - } - - return errors; - }, + validate: async (values) => validateChangePasswordForm(values, isCurrentUser), defaultValues, }); @@ -246,6 +244,7 @@ export const ChangePasswordFlyout: FunctionComponent isInvalid={form.touched.current_password && !!form.errors.current_password} autoComplete="current-password" inputRef={firstFieldRef} + data-test-subj="editUserChangePasswordCurrentPasswordInput" /> ) : null} @@ -268,6 +267,7 @@ export const ChangePasswordFlyout: FunctionComponent isInvalid={form.touched.password && !!form.errors.password} autoComplete="new-password" inputRef={isCurrentUser ? undefined : firstFieldRef} + data-test-subj="editUserChangePasswordNewPasswordInput" /> defaultValue={form.values.confirm_password} isInvalid={form.touched.confirm_password && !!form.errors.confirm_password} autoComplete="new-password" + data-test-subj="editUserChangePasswordConfirmPasswordInput" /> diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index b8e98042b1cff..66a73d9c6aa87 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -5,21 +5,16 @@ * 2.0. */ -import { fireEvent, render, waitFor, within } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React from 'react'; import { coreMock } from 'src/core/public/mocks'; -import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; import { securityMock } from '../../../mocks'; import { Providers } from '../users_management_app'; import { EditUserPage } from './edit_user_page'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => `id-${Math.random()}`, -})); - const userMock = { username: 'jdoe', full_name: '', @@ -28,13 +23,7 @@ const userMock = { roles: ['superuser'], }; -// FLAKY: https://github.com/elastic/kibana/issues/115473 -// FLAKY: https://github.com/elastic/kibana/issues/115474 -// FLAKY: https://github.com/elastic/kibana/issues/116889 -// FLAKY: https://github.com/elastic/kibana/issues/117081 -// FLAKY: https://github.com/elastic/kibana/issues/116891 -// FLAKY: https://github.com/elastic/kibana/issues/116890 -describe.skip('EditUserPage', () => { +describe('EditUserPage', () => { const coreStart = coreMock.createStart(); let history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); const authc = securityMock.createSetup().authc; @@ -135,263 +124,4 @@ describe.skip('EditUserPage', () => { await findByText(/Role .deprecated_role. is deprecated. Use .new_role. instead/i); }); - - it('updates user when submitting form and redirects back', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole, findByLabelText } = render( - - - - ); - - fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); - fireEvent.change(await findByLabelText('Email address'), { - target: { value: 'jdoe@elastic.co' }, - }); - fireEvent.click(await findByRole('button', { name: 'Update user' })); - - await waitFor(() => { - expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); - expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { - body: JSON.stringify({ - ...userMock, - full_name: 'John Doe', - email: 'jdoe@elastic.co', - }), - }); - expect(history.location.pathname).toBe('/'); - }); - }); - - it('warns when user form submission fails', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); - - const { findByRole, findByLabelText } = render( - - - - ); - - fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); - fireEvent.change(await findByLabelText('Email address'), { - target: { value: 'jdoe@elastic.co' }, - }); - fireEvent.click(await findByRole('button', { name: 'Update user' })); - - await waitFor(() => { - expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); - expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { - body: JSON.stringify({ - ...userMock, - full_name: 'John Doe', - email: 'jdoe@elastic.co', - }), - }); - expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ - text: 'Error message', - title: "Could not update user 'jdoe'", - }); - expect(history.location.pathname).toBe('/edit/jdoe'); - }); - }); - - it('changes password of other user when submitting form and closes dialog', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - authc.getCurrentUser.mockResolvedValueOnce( - mockAuthenticatedUser({ ...userMock, username: 'elastic' }) - ); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: 'changeme' }, - }); - fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { - target: { value: 'changeme' }, - }); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - - expect(await findByRole('dialog')).not.toBeInTheDocument(); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { - body: JSON.stringify({ - newPassword: 'changeme', - }), - }); - }); - - it('changes password of current user when submitting form and closes dialog', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); - fireEvent.change(await within(dialog).findByLabelText('Current password'), { - target: { value: '123456' }, - }); - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: 'changeme' }, - }); - fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { - target: { value: 'changeme' }, - }); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - - expect(await findByRole('dialog')).not.toBeInTheDocument(); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { - body: JSON.stringify({ - newPassword: 'changeme', - password: '123456', - }), - }); - }); - - it('warns when change password form submission fails', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - authc.getCurrentUser.mockResolvedValueOnce( - mockAuthenticatedUser({ ...userMock, username: 'elastic' }) - ); - coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: 'changeme' }, - }); - fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { - target: { value: 'changeme' }, - }); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - - await waitFor(() => { - expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ - text: 'Error message', - title: 'Could not change password', - }); - }); - }); - - it('validates change password form', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Change password' })); - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - await within(dialog).findByText(/Enter your current password/i); - await within(dialog).findByText(/Enter a new password/i); - - fireEvent.change(await within(dialog).findByLabelText('Current password'), { - target: { value: 'changeme' }, - }); - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: '111' }, - }); - await within(dialog).findAllByText(/Password must be at least 6 characters/i); - - fireEvent.change(await within(dialog).findByLabelText('New password'), { - target: { value: '123456' }, - }); - fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { - target: { value: '111' }, - }); - await within(dialog).findAllByText(/Passwords do not match/i); - }); - - it('deactivates user when confirming and closes dialog', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Deactivate user' })); - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Deactivate user' })); - - expect(await findByRole('dialog')).not.toBeInTheDocument(); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_disable'); - }); - - it('activates user when confirming and closes dialog', async () => { - coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false }); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.post.mockResolvedValueOnce({}); - - const { findByRole, findAllByRole } = render( - - - - ); - - const [enableButton] = await findAllByRole('button', { name: 'Activate user' }); - fireEvent.click(enableButton); - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Activate user' })); - - expect(await findByRole('dialog')).not.toBeInTheDocument(); - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable'); - }); - - it('deletes user when confirming and redirects back', async () => { - coreStart.http.get.mockResolvedValueOnce(userMock); - coreStart.http.get.mockResolvedValueOnce([]); - coreStart.http.delete.mockResolvedValueOnce({}); - - const { findByRole } = render( - - - - ); - - fireEvent.click(await findByRole('button', { name: 'Delete user' })); - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete user' })); - - await waitFor(() => { - expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); - expect(history.location.pathname).toBe('/'); - }); - }); }); diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 66216da49be95..8887ec93ff58d 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -211,7 +211,11 @@ export const EditUserPage: FunctionComponent = ({ username }) - setAction('changePassword')} size="s"> + setAction('changePassword')} + size="s" + data-test-subj="editUserChangePasswordButton" + > = ({ username }) - setAction('enableUser')} size="s"> + setAction('enableUser')} + size="s" + data-test-subj="editUserEnableUserButton" + > = ({ username }) - setAction('disableUser')} size="s"> + setAction('disableUser')} + size="s" + data-test-subj="editUserDisableUserButton" + > = ({ username }) - setAction('deleteUser')} size="s" color="danger"> + setAction('deleteUser')} + size="s" + color="danger" + data-test-subj="editUserDeleteUserButton" + > { log.debug('users'); await PageObjects.settings.navigateTo(); @@ -44,32 +56,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should add new user', async function () { - await PageObjects.security.createUser({ + const userLee: UserFormValues = { username: 'Lee', password: 'LeePwd', confirm_password: 'LeePwd', full_name: 'LeeFirst LeeLast', email: 'lee@myEmail.com', roles: ['kibana_admin'], - }); + }; + await PageObjects.security.createUser(userLee); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.Lee.roles).to.eql(['kibana_admin']); - expect(users.Lee.fullname).to.eql('LeeFirst LeeLast'); - expect(users.Lee.email).to.eql('lee@myEmail.com'); + expect(users.Lee.roles).to.eql(userLee.roles); + expect(users.Lee.fullname).to.eql(userLee.full_name); + expect(users.Lee.email).to.eql(userLee.email); expect(users.Lee.reserved).to.be(false); }); it('should add new user with optional fields left empty', async function () { - await PageObjects.security.createUser({ - username: 'OptionalUser', - password: 'OptionalUserPwd', - confirm_password: 'OptionalUserPwd', - roles: [], - }); + await PageObjects.security.createUser(optionalUser); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.OptionalUser.roles).to.eql(['']); + expect(users.OptionalUser.roles).to.eql(optionalUser.roles); expect(users.OptionalUser.fullname).to.eql(''); expect(users.OptionalUser.email).to.eql(''); expect(users.OptionalUser.reserved).to.be(false); @@ -115,5 +123,101 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(roles.monitoring_user.reserved).to.be(true); expect(roles.monitoring_user.deprecated).to.be(false); }); + + describe('edit', function () { + before(async () => { + await PageObjects.security.clickElasticsearchUsers(); + }); + + describe('update user profile', () => { + it('when submitting form and redirects back', async () => { + optionalUser.full_name = 'Optional User'; + optionalUser.email = 'optionalUser@elastic.co'; + + await PageObjects.security.updateUserProfile(optionalUser); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + + expect(users.OptionalUser.roles).to.eql(optionalUser.roles); + expect(users.OptionalUser.fullname).to.eql(optionalUser.full_name); + expect(users.OptionalUser.email).to.eql(optionalUser.email); + expect(users.OptionalUser.reserved).to.be(false); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/users'); + }); + }); + + describe('change password', () => { + before(async () => { + await toasts.dismissAllToasts(); + }); + afterEach(async () => { + await PageObjects.security.submitUpdateUserForm(); + await toasts.dismissAllToasts(); + }); + after(async () => { + await PageObjects.security.forceLogout(); + await PageObjects.security.login(); + await PageObjects.settings.navigateTo(); + await PageObjects.security.clickElasticsearchUsers(); + }); + it('of other user when submitting form', async () => { + optionalUser.password = 'NewOptionalUserPwd'; + optionalUser.confirm_password = 'NewOptionalUserPwd'; + + await PageObjects.security.updateUserPassword(optionalUser); + await retry.waitFor('', async () => { + const toastCount = await toasts.getToastCount(); + return toastCount >= 1; + }); + const successToast = await toasts.getToastElement(1); + expect(await successToast.getVisibleText()).to.be( + `Password changed for '${optionalUser.username}'.` + ); + }); + + it('of current user when submitting form', async () => { + optionalUser.current_password = 'NewOptionalUserPwd'; + optionalUser.password = 'NewOptionalUserPwd_2'; + optionalUser.confirm_password = 'NewOptionalUserPwd_2'; + + await PageObjects.security.forceLogout(); + await PageObjects.security.login(optionalUser.username, optionalUser.current_password); + await PageObjects.settings.navigateTo(); + await PageObjects.security.clickElasticsearchUsers(); + + await PageObjects.security.updateUserPassword(optionalUser, true); + await retry.waitFor('', async () => { + const toastCount = await toasts.getToastCount(); + return toastCount >= 1; + }); + const successToast = await toasts.getToastElement(1); + expect(await successToast.getVisibleText()).to.be( + `Password changed for '${optionalUser.username}'.` + ); + }); + }); + + describe('Deactivate/Activate user', () => { + it('deactivates user when confirming', async () => { + await PageObjects.security.deactivatesUser(optionalUser); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + expect(users.OptionalUser.enabled).to.be(false); + }); + + it('activates user when confirming', async () => { + await PageObjects.security.activatesUser(optionalUser); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + expect(users.OptionalUser.enabled).to.be(true); + }); + }); + + describe('Delete user', () => { + it('when confirming and closes dialog', async () => { + await PageObjects.security.deleteUser(optionalUser.username ?? ''); + const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); + expect(users).to.not.have.key(optionalUser.username ?? ''); + expect(await browser.getCurrentUrl()).to.contain('app/management/security/users'); + }); + }); + }); }); } diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 37fe0eccea31a..274de53c5f3fd 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -389,7 +389,7 @@ export class SecurityPageObject extends FtrService { // findAll is substantially faster than `find.descendantExistsByCssSelector for negative cases const isUserReserved = (await user.findAllByTestSubject('userReserved', 1)).length > 0; const isUserDeprecated = (await user.findAllByTestSubject('userDeprecated', 1)).length > 0; - + const isEnabled = (await user.findAllByTestSubject('userDisabled', 1)).length === 0; users.push({ username: await usernameElement.getVisibleText(), fullname: await fullnameElement.getVisibleText(), @@ -397,6 +397,7 @@ export class SecurityPageObject extends FtrService { roles: (await rolesElement.getVisibleText()).split('\n').map((role) => role.trim()), reserved: isUserReserved, deprecated: isUserDeprecated, + enabled: isEnabled, }); } @@ -459,10 +460,23 @@ export class SecurityPageObject extends FtrService { } } + async updateUserProfileForm(user: UserFormValues) { + if (user.full_name) { + await this.find.setValue('[name=full_name]', user.full_name); + } + if (user.email) { + await this.find.setValue('[name=email]', user.email); + } + } + async submitCreateUserForm() { await this.find.clickByButtonText('Create user'); } + async submitUpdateUserForm() { + await this.find.clickByButtonText('Update user'); + } + async createUser(user: UserFormValues) { await this.clickElasticsearchUsers(); await this.clickCreateNewUser(); @@ -470,6 +484,62 @@ export class SecurityPageObject extends FtrService { await this.submitCreateUserForm(); } + async clickUserByUserName(username: string) { + await this.find.clickByDisplayedLinkText(username); + await this.header.awaitGlobalLoadingIndicatorHidden(); + } + + async updateUserPassword(user: UserFormValues, isCurrentUser: boolean = false) { + await this.clickUserByUserName(user.username ?? ''); + await this.testSubjects.click('editUserChangePasswordButton'); + if (isCurrentUser) { + await this.testSubjects.setValue( + 'editUserChangePasswordCurrentPasswordInput', + user.current_password ?? '' + ); + } + await this.testSubjects.setValue('editUserChangePasswordNewPasswordInput', user.password ?? ''); + await this.testSubjects.setValue( + 'editUserChangePasswordConfirmPasswordInput', + user.confirm_password ?? '' + ); + await this.testSubjects.click('formFlyoutSubmitButton'); + } + + async updateUserProfile(user: UserFormValues) { + await this.clickUserByUserName(user.username ?? ''); + await this.updateUserProfileForm(user); + await this.submitUpdateUserForm(); + } + + async deactivatesUser(user: UserFormValues) { + await this.clickUserByUserName(user.username ?? ''); + await this.testSubjects.click('editUserDisableUserButton'); + await this.testSubjects.click('confirmModalConfirmButton'); + await this.submitUpdateUserForm(); + } + + async activatesUser(user: UserFormValues) { + await this.clickUserByUserName(user.username ?? ''); + await this.testSubjects.click('editUserEnableUserButton'); + await this.testSubjects.click('confirmModalConfirmButton'); + await this.submitUpdateUserForm(); + } + + async deleteUser(username: string) { + this.log.debug('Delete user ' + username); + await this.clickUserByUserName(username); + + this.log.debug('Find delete button and click'); + await this.find.clickByButtonText('Delete user'); + await this.common.sleep(2000); + + const confirmText = await this.testSubjects.getVisibleText('confirmModalBodyText'); + this.log.debug('Delete user alert text = ' + confirmText); + await this.testSubjects.click('confirmModalConfirmButton'); + return confirmText; + } + async addRole(roleName: string, roleObj: Role) { const self = this; @@ -548,19 +618,4 @@ export class SecurityPageObject extends FtrService { await this.find.clickByCssSelector(`[role=option][title="${role}"]`); await this.testSubjects.click('comboBoxToggleListButton'); } - - async deleteUser(username: string) { - this.log.debug('Delete user ' + username); - await this.find.clickByDisplayedLinkText(username); - await this.header.awaitGlobalLoadingIndicatorHidden(); - - this.log.debug('Find delete button and click'); - await this.find.clickByButtonText('Delete user'); - await this.common.sleep(2000); - - const confirmText = await this.testSubjects.getVisibleText('confirmModalBodyText'); - this.log.debug('Delete user alert text = ' + confirmText); - await this.testSubjects.click('confirmModalConfirmButton'); - return confirmText; - } } From b1e48fb96a9085a9df5a5dc20cee867360ffeef4 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Mon, 8 Nov 2021 11:43:29 -0600 Subject: [PATCH 06/98] [ML] Display managed badge for transforms (#117679) * [ML] Add managed badge * Fix i18n Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/transform_list/use_columns.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index bad42c212293d..4e269c2f42729 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -144,6 +144,20 @@ export const useColumns = ( sortable: true, truncateText: true, scope: 'row', + render: (transformId, item) => { + if (item.config?._meta?.managed !== true) return transformId; + return ( + <> + {transformId} +   + + {i18n.translate('xpack.transform.transformList.managedBadgeLabel', { + defaultMessage: 'Managed', + })} + + + ); + }, }, { id: 'alertRule', From 635cad481728e584f0dced07b47e7636150f17f3 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 8 Nov 2021 10:45:57 -0700 Subject: [PATCH 07/98] [Maps] rename VectorLayer to GeoJsonVectorLayer and TiledVectorLayer to MvtVectorLayer (#117207) * [Maps] rename VectorLayer to GeoJsonVectorLayer and TiledVectorLayer to MvtVectorLayer * fix jest test * eslint * review feedback * eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/common/constants.ts | 1 - .../maps/public/actions/map_actions.ts | 10 +- .../create_choropleth_layer_descriptor.ts | 4 +- .../create_region_map_layer_descriptor.ts | 4 +- .../create_tile_map_layer_descriptor.ts | 4 +- .../layers/file_upload_wizard/wizard.tsx | 4 +- .../layers/heatmap_layer/heatmap_layer.ts | 4 +- .../layers/new_vector_layer_wizard/wizard.tsx | 4 +- .../observability/create_layer_descriptor.ts | 6 +- .../security/create_layer_descriptors.ts | 8 +- .../classes/layers/tile_layer/tile_layer.js | 4 +- .../blended_vector_layer.test.tsx | 8 +- .../blended_vector_layer.ts | 41 +- .../blended_vector_layer/index.ts | 8 + .../assign_feature_ids.test.ts | 0 .../assign_feature_ids.ts | 0 .../geojson_vector_layer.tsx | 405 ++++++++++++++++++ .../get_centroid_features.test.ts | 0 .../get_centroid_features.ts | 2 +- .../geojson_vector_layer/index.ts | 8 + .../perform_inner_joins.test.ts | 10 +- .../perform_inner_joins.ts | 8 +- .../{ => geojson_vector_layer}/utils.tsx | 14 +- .../classes/layers/vector_layer/index.ts | 12 +- .../mvt_vector_layer.test.tsx.snap} | 0 .../vector_layer/mvt_vector_layer/index.ts | 8 + .../mvt_vector_layer.test.tsx} | 38 +- .../mvt_vector_layer/mvt_vector_layer.tsx} | 27 +- .../layers/vector_layer/vector_layer.test.tsx | 10 +- .../layers/vector_layer/vector_layer.tsx | 378 +--------------- .../ems_boundaries_layer_wizard.tsx | 4 +- .../clusters_layer_wizard.tsx | 4 +- .../es_geo_line_source/layer_wizard.tsx | 4 +- .../point_2_point_layer_wizard.tsx | 4 +- .../create_layer_descriptor.ts | 4 +- .../es_documents_layer_wizard.tsx | 8 +- .../es_search_source/top_hits/wizard.tsx | 4 +- .../layer_wizard.tsx | 4 +- .../connected_components/mb_map/mb_map.tsx | 4 +- .../toc_entry_actions_popover.tsx | 8 +- .../public/selectors/map_selectors.test.ts | 2 - .../maps/public/selectors/map_selectors.ts | 30 +- 42 files changed, 597 insertions(+), 513 deletions(-) rename x-pack/plugins/maps/public/classes/layers/{ => vector_layer}/blended_vector_layer/blended_vector_layer.test.tsx (95%) rename x-pack/plugins/maps/public/classes/layers/{ => vector_layer}/blended_vector_layer/blended_vector_layer.ts (88%) create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/index.ts rename x-pack/plugins/maps/public/classes/layers/vector_layer/{ => geojson_vector_layer}/assign_feature_ids.test.ts (100%) rename x-pack/plugins/maps/public/classes/layers/vector_layer/{ => geojson_vector_layer}/assign_feature_ids.ts (100%) create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx rename x-pack/plugins/maps/public/classes/layers/vector_layer/{ => geojson_vector_layer}/get_centroid_features.test.ts (100%) rename x-pack/plugins/maps/public/classes/layers/vector_layer/{ => geojson_vector_layer}/get_centroid_features.ts (99%) create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/index.ts rename x-pack/plugins/maps/public/classes/layers/vector_layer/{ => geojson_vector_layer}/perform_inner_joins.test.ts (95%) rename x-pack/plugins/maps/public/classes/layers/vector_layer/{ => geojson_vector_layer}/perform_inner_joins.ts (94%) rename x-pack/plugins/maps/public/classes/layers/vector_layer/{ => geojson_vector_layer}/utils.tsx (92%) rename x-pack/plugins/maps/public/classes/layers/{tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap => vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap} (100%) create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/index.ts rename x-pack/plugins/maps/public/classes/layers/{tiled_vector_layer/tiled_vector_layer.test.tsx => vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx} (85%) rename x-pack/plugins/maps/public/classes/layers/{tiled_vector_layer/tiled_vector_layer.tsx => vector_layer/mvt_vector_layer/mvt_vector_layer.tsx} (95%) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 5de21099c9340..0f2ce2c917738 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -85,7 +85,6 @@ export const SOURCE_DATA_REQUEST_ID = 'source'; export const SOURCE_META_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${META_DATA_REQUEST_ID_SUFFIX}`; export const SOURCE_FORMATTERS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${FORMATTERS_DATA_REQUEST_ID_SUFFIX}`; export const SOURCE_BOUNDS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_bounds`; -export const SUPPORTS_FEATURE_EDITING_REQUEST_ID = 'SUPPORTS_FEATURE_EDITING_REQUEST_ID'; export const MIN_ZOOM = 0; export const MAX_ZOOM = 24; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index d921f9748f65c..9ec9a42986fbb 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -61,7 +61,7 @@ import { MapSettings } from '../reducers/map'; import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; import { updateTooltipStateForLayer } from './tooltip_actions'; -import { VectorLayer } from '../classes/layers/vector_layer'; +import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE } from './ui_actions'; import { expandToTileBoundaries } from '../classes/util/geo_tile_utils'; import { getToasts } from '../kibana_services'; @@ -357,12 +357,12 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { return; } const layer = getLayerById(layerId, getState()); - if (!layer || !(layer instanceof VectorLayer)) { + if (!layer || !isVectorLayer(layer)) { return; } try { - await layer.addFeature(geometry); + await (layer as IVectorLayer).addFeature(geometry); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { @@ -385,11 +385,11 @@ export function deleteFeatureFromIndex(featureId: string) { return; } const layer = getLayerById(layerId, getState()); - if (!layer || !(layer instanceof VectorLayer)) { + if (!layer || !isVectorLayer(layer)) { return; } try { - await layer.deleteFeature(featureId); + await (layer as IVectorLayer).deleteFeature(featureId); await dispatch(syncDataForLayerDueToDrawing(layer)); } catch (e) { getToasts().addError(e, { diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts index 6b91e4812a1d6..ad507aa171631 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts @@ -23,7 +23,7 @@ import { ESSearchSourceDescriptor, } from '../../../../common/descriptor_types'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../vector_layer'; +import { GeoJsonVectorLayer } from '../vector_layer'; import { EMSFileSource } from '../../sources/ems_file_source'; // @ts-ignore import { ESSearchSource } from '../../sources/es_search_source'; @@ -51,7 +51,7 @@ function createChoroplethLayerDescriptor({ aggFieldName: '', rightSourceId: joinId, }); - return VectorLayer.createDescriptor({ + return GeoJsonVectorLayer.createDescriptor({ joins: [ { leftField, diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts index 408460de28aeb..19d9567a3480a 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -22,7 +22,7 @@ import { } from '../../../common/constants'; import { VectorStyle } from '../styles/vector/vector_style'; import { EMSFileSource } from '../sources/ems_file_source'; -import { VectorLayer } from './vector_layer'; +import { GeoJsonVectorLayer } from './vector_layer'; import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes'; import { getJoinAggKey } from '../../../common/get_agg_key'; @@ -97,7 +97,7 @@ export function createRegionMapLayerDescriptor({ if (termsSize !== undefined) { termSourceDescriptor.size = termsSize; } - return VectorLayer.createDescriptor({ + return GeoJsonVectorLayer.createDescriptor({ label, joins: [ { diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts index 98217a5f28ad8..676ba4e8c88b1 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts @@ -24,7 +24,7 @@ import { } from '../../../common/constants'; import { VectorStyle } from '../styles/vector/vector_style'; import { ESGeoGridSource } from '../sources/es_geo_grid_source'; -import { VectorLayer } from './vector_layer'; +import { GeoJsonVectorLayer } from './vector_layer'; import { HeatmapLayer } from './heatmap_layer'; import { getDefaultDynamicProperties } from '../styles/vector/vector_style_defaults'; import { NUMERICAL_COLOR_PALETTES } from '../styles/color_palettes'; @@ -162,7 +162,7 @@ export function createTileMapLayerDescriptor({ }; } - return VectorLayer.createDescriptor({ + return GeoJsonVectorLayer.createDescriptor({ label, sourceDescriptor: geoGridSourceDescriptor, style: VectorStyle.createDescriptor(styleProperties), diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 87747d915af4a..f29a4c3a55e28 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -12,7 +12,7 @@ import { FeatureCollection } from 'geojson'; import { EuiPanel } from '@elastic/eui'; import { DEFAULT_MAX_RESULT_WINDOW, SCALING_TYPES } from '../../../../common/constants'; import { GeoJsonFileSource } from '../../sources/geojson_file_source'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { FileUploadGeoResults } from '../../../../../file_upload/public'; @@ -113,7 +113,7 @@ export class ClientFileCreateSourceEditor extends Component) { const heatmapLayerDescriptor = super.createDescriptor(options); - heatmapLayerDescriptor.type = HeatmapLayer.type; + heatmapLayerDescriptor.type = LAYER_TYPE.HEATMAP; heatmapLayerDescriptor.style = HeatmapStyle.createDescriptor(); return heatmapLayerDescriptor; } diff --git a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx index 100e9dfa45c1d..d26853850c387 100644 --- a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx @@ -10,7 +10,7 @@ import { EuiPanel, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { createNewIndexAndPattern } from './create_new_index_pattern'; import { RenderWizardArguments } from '../layer_wizard_registry'; -import { VectorLayer } from '../vector_layer'; +import { GeoJsonVectorLayer } from '../vector_layer'; import { ESSearchSource } from '../../sources/es_search_source'; import { ADD_LAYER_STEP_ID } from '../../../connected_components/add_layer_panel/view'; import { getFileUpload, getIndexNameFormComponent } from '../../../kibana_services'; @@ -127,7 +127,7 @@ export class NewVectorLayerEditor extends Component { +jest.mock('../../../../kibana_services', () => { return { getIsDarkMode() { return false; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts similarity index 88% rename from x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts index a158892be9d09..e4c0ccdca09a4 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts @@ -6,11 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { IVectorLayer, VectorLayer } from '../vector_layer'; -import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; -import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; -import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -import { IStyleProperty } from '../../styles/vector/properties/style_property'; +import { IVectorLayer } from '../vector_layer'; +import { GeoJsonVectorLayer } from '../geojson_vector_layer'; +import { IVectorStyle, VectorStyle } from '../../../styles/vector/vector_style'; +import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style_defaults'; +import { IDynamicStyleProperty } from '../../../styles/vector/properties/dynamic_style_property'; +import { IStyleProperty } from '../../../styles/vector/properties/style_property'; import { COUNT_PROP_LABEL, COUNT_PROP_NAME, @@ -21,13 +22,13 @@ import { VECTOR_STYLES, LAYER_STYLE_TYPE, FIELD_ORIGIN, -} from '../../../../common/constants'; -import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; -import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; -import { IESSource } from '../../sources/es_source'; -import { ISource } from '../../sources/source'; -import { DataRequestContext } from '../../../actions'; -import { DataRequestAbortError } from '../../util/data_request'; +} from '../../../../../common/constants'; +import { ESGeoGridSource } from '../../../sources/es_geo_grid_source/es_geo_grid_source'; +import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; +import { IESSource } from '../../../sources/es_source'; +import { ISource } from '../../../sources/source'; +import { DataRequestContext } from '../../../../actions'; +import { DataRequestAbortError } from '../../../util/data_request'; import { VectorStyleDescriptor, SizeDynamicOptions, @@ -37,11 +38,11 @@ import { VectorLayerDescriptor, VectorSourceRequestMeta, VectorStylePropertiesDescriptor, -} from '../../../../common/descriptor_types'; -import { IVectorSource } from '../../sources/vector_source'; -import { LICENSED_FEATURES } from '../../../licensed_features'; -import { ESSearchSource } from '../../sources/es_search_source/es_search_source'; -import { isSearchSourceAbortError } from '../../sources/es_source/es_source'; +} from '../../../../../common/descriptor_types'; +import { IVectorSource } from '../../../sources/vector_source'; +import { LICENSED_FEATURES } from '../../../../licensed_features'; +import { ESSearchSource } from '../../../sources/es_search_source/es_search_source'; +import { isSearchSourceAbortError } from '../../../sources/es_source/es_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; @@ -170,14 +171,12 @@ export interface BlendedVectorLayerArguments { layerDescriptor: VectorLayerDescriptor; } -export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { - static type = LAYER_TYPE.BLENDED_VECTOR; - +export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLayer { static createDescriptor( options: Partial, mapColors: string[] ): VectorLayerDescriptor { - const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); + const layerDescriptor = GeoJsonVectorLayer.createDescriptor(options, mapColors); layerDescriptor.type = LAYER_TYPE.BLENDED_VECTOR; return layerDescriptor; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/index.ts new file mode 100644 index 0000000000000..a8079e06358d5 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/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 { BlendedVectorLayer } from './blended_vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.test.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.test.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.test.ts diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/assign_feature_ids.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/assign_feature_ids.ts diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx new file mode 100644 index 0000000000000..80da6ceecf3a6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx @@ -0,0 +1,405 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiIcon } from '@elastic/eui'; +import { Feature, FeatureCollection } from 'geojson'; +import type { Map as MbMap, GeoJSONSource as MbGeoJSONSource } from '@kbn/mapbox-gl'; +import { + EMPTY_FEATURE_COLLECTION, + FEATURE_VISIBLE_PROPERTY_NAME, + FIELD_ORIGIN, + LAYER_TYPE, +} from '../../../../../common/constants'; +import { + StyleMetaDescriptor, + Timeslice, + VectorJoinSourceRequestMeta, + VectorLayerDescriptor, +} from '../../../../../common/descriptor_types'; +import { PropertiesMap } from '../../../../../common/elasticsearch_util'; +import { TimesliceMaskConfig } from '../../../util/mb_filter_expressions'; +import { DataRequestContext } from '../../../../actions'; +import { IVectorStyle, VectorStyle } from '../../../styles/vector/vector_style'; +import { ISource } from '../../../sources/source'; +import { IVectorSource } from '../../../sources/vector_source'; +import { AbstractLayer, CustomIconAndTooltipContent } from '../../layer'; +import { InnerJoin } from '../../../joins/inner_join'; +import { + AbstractVectorLayer, + noResultsIcon, + NO_RESULTS_ICON_AND_TOOLTIPCONTENT, +} from '../vector_layer'; +import { DataRequestAbortError } from '../../../util/data_request'; +import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; +import { getFeatureCollectionBounds } from '../../../util/get_feature_collection_bounds'; +import { GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids'; +import { addGeoJsonMbSource, syncVectorSource } from './utils'; +import { JoinState, performInnerJoins } from './perform_inner_joins'; +import { buildVectorRequestMeta } from '../../build_vector_request_meta'; + +export const SUPPORTS_FEATURE_EDITING_REQUEST_ID = 'SUPPORTS_FEATURE_EDITING_REQUEST_ID'; + +export class GeoJsonVectorLayer extends AbstractVectorLayer { + static createDescriptor( + options: Partial, + mapColors?: string[] + ): VectorLayerDescriptor { + const layerDescriptor = super.createDescriptor(options) as VectorLayerDescriptor; + layerDescriptor.type = LAYER_TYPE.VECTOR; + + if (!options.style) { + const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); + layerDescriptor.style = VectorStyle.createDescriptor(styleProperties); + } + + if (!options.joins) { + layerDescriptor.joins = []; + } + + return layerDescriptor; + } + + supportsFeatureEditing(): boolean { + const dataRequest = this.getDataRequest(SUPPORTS_FEATURE_EDITING_REQUEST_ID); + const data = dataRequest?.getData() as { supportsFeatureEditing: boolean } | undefined; + return data ? data.supportsFeatureEditing : false; + } + + async getBounds(syncContext: DataRequestContext) { + const isStaticLayer = !this.getSource().isBoundsAware(); + return isStaticLayer || this.hasJoins() + ? getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()) + : super.getBounds(syncContext); + } + + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { + const featureCollection = this._getSourceFeatureCollection(); + + if (!featureCollection || featureCollection.features.length === 0) { + return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; + } + + if ( + this.getJoins().length && + !featureCollection.features.some( + (feature) => feature.properties?.[FEATURE_VISIBLE_PROPERTY_NAME] + ) + ) { + return { + icon: noResultsIcon, + tooltipContent: i18n.translate('xpack.maps.vectorLayer.noResultsFoundInJoinTooltip', { + defaultMessage: `No matching results found in term joins`, + }), + }; + } + + const sourceDataRequest = this.getSourceDataRequest(); + const { tooltipContent, areResultsTrimmed, isDeprecated } = + this.getSource().getSourceTooltipContent(sourceDataRequest); + return { + icon: isDeprecated ? ( + + ) : ( + this.getCurrentStyle().getIcon() + ), + tooltipContent, + areResultsTrimmed, + }; + } + + getFeatureId(feature: Feature): string | number | undefined { + return feature.properties?.[GEOJSON_FEATURE_ID_PROPERTY_NAME]; + } + + getFeatureById(id: string | number) { + const featureCollection = this._getSourceFeatureCollection(); + if (!featureCollection) { + return null; + } + + const targetFeature = featureCollection.features.find((feature) => { + return this.getFeatureId(feature) === id; + }); + return targetFeature ? targetFeature : null; + } + + async getStyleMetaDescriptorFromLocalFeatures(): Promise { + const sourceDataRequest = this.getSourceDataRequest(); + const style = this.getCurrentStyle(); + if (!style || !sourceDataRequest) { + return null; + } + return await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); + } + + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) { + addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); + + this._syncFeatureCollectionWithMb(mbMap); + + const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice); + this._setMbLabelProperties(mbMap, undefined, timesliceMaskConfig); + this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig); + this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig); + } + + _syncFeatureCollectionWithMb(mbMap: MbMap) { + const mbGeoJSONSource = mbMap.getSource(this.getId()) as MbGeoJSONSource; + const featureCollection = this._getSourceFeatureCollection(); + const featureCollectionOnMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); + + if (!featureCollection) { + if (featureCollectionOnMap) { + this.getCurrentStyle().clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); + } + mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION); + return; + } + + // "feature-state" data expressions are not supported with layout properties. + // To work around this limitation, + // scaled layout properties (like icon-size) must fall back to geojson property values :( + const hasGeoJsonProperties = this.getCurrentStyle().setFeatureStateAndStyleProps( + featureCollection, + mbMap, + this.getId() + ); + if (featureCollection !== featureCollectionOnMap || hasGeoJsonProperties) { + mbGeoJSONSource.setData(featureCollection); + } + } + + _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined { + if (!timeslice || this.hasJoins()) { + return; + } + + const prevMeta = this.getSourceDataRequest()?.getMeta(); + return prevMeta !== undefined && prevMeta.timesliceMaskField !== undefined + ? { + timesliceMaskField: prevMeta.timesliceMaskField, + timeslice, + } + : undefined; + } + + async syncData(syncContext: DataRequestContext) { + await this._syncData(syncContext, this.getSource(), this.getCurrentStyle()); + } + + // TLDR: Do not call getSource or getCurrentStyle in syncData flow. Use 'source' and 'style' arguments instead. + // + // 1) State is contained in the redux store. Layer instance state is readonly. + // 2) Even though data request descriptor updates trigger new instances for rendering, + // syncing data executes on a single object instance. Syncing data can not use updated redux store state. + // + // Blended layer data syncing branches on the source/style depending on whether clustering is used or not. + // Given 1 above, which source/style to use can not be stored in Layer instance state. + // Given 2 above, which source/style to use can not be pulled from data request state. + // Therefore, source and style are provided as arugments and must be used instead of calling getSource or getCurrentStyle. + async _syncData(syncContext: DataRequestContext, source: IVectorSource, style: IVectorStyle) { + if (this.isLoadingBounds()) { + return; + } + + try { + await this._syncSourceStyleMeta(syncContext, source, style); + await this._syncSourceFormatters(syncContext, source, style); + const sourceResult = await syncVectorSource({ + layerId: this.getId(), + layerName: await this.getDisplayName(source), + prevDataRequest: this.getSourceDataRequest(), + requestMeta: await this._getVectorSourceRequestMeta( + syncContext.isForceRefresh, + syncContext.dataFilters, + source, + style + ), + syncContext, + source, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); + }, + }); + await this._syncSupportsFeatureEditing({ syncContext, source }); + if ( + !sourceResult.featureCollection || + !sourceResult.featureCollection.features.length || + !this.hasJoins() + ) { + return; + } + + const joinStates = await this._syncJoins(syncContext, style); + performInnerJoins( + sourceResult, + joinStates, + syncContext.updateSourceData, + syncContext.onJoinError + ); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + throw error; + } + } + } + + async _syncJoin({ + join, + startLoading, + stopLoading, + onLoadError, + registerCancelCallback, + dataFilters, + isForceRefresh, + }: { join: InnerJoin } & DataRequestContext): Promise { + const joinSource = join.getRightJoinSource(); + const sourceDataId = join.getSourceDataRequestId(); + const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); + + const joinRequestMeta: VectorJoinSourceRequestMeta = buildVectorRequestMeta( + joinSource, + joinSource.getFieldNames(), + dataFilters, + joinSource.getWhereQuery(), + isForceRefresh + ) as VectorJoinSourceRequestMeta; + + const prevDataRequest = this.getDataRequest(sourceDataId); + const canSkipFetch = await canSkipSourceUpdate({ + source: joinSource, + prevDataRequest, + nextRequestMeta: joinRequestMeta, + extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource). + getUpdateDueToTimeslice: () => { + return true; + }, + }); + + if (canSkipFetch) { + return { + dataHasChanged: false, + join, + propertiesMap: prevDataRequest?.getData() as PropertiesMap, + }; + } + + try { + startLoading(sourceDataId, requestToken, joinRequestMeta); + const leftSourceName = await this._source.getDisplayName(); + const propertiesMap = await joinSource.getPropertiesMap( + joinRequestMeta, + leftSourceName, + join.getLeftField().getName(), + registerCancelCallback.bind(null, requestToken) + ); + stopLoading(sourceDataId, requestToken, propertiesMap); + return { + dataHasChanged: true, + join, + propertiesMap, + }; + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + onLoadError(sourceDataId, requestToken, `Join error: ${error.message}`); + } + throw error; + } + } + + async _syncJoins(syncContext: DataRequestContext, style: IVectorStyle) { + const joinSyncs = this.getValidJoins().map(async (join) => { + await this._syncJoinStyleMeta(syncContext, join, style); + await this._syncJoinFormatters(syncContext, join, style); + return this._syncJoin({ join, ...syncContext }); + }); + + return await Promise.all(joinSyncs); + } + + async _syncJoinStyleMeta(syncContext: DataRequestContext, join: InnerJoin, style: IVectorStyle) { + const joinSource = join.getRightJoinSource(); + return this._syncStyleMeta({ + source: joinSource, + style, + sourceQuery: joinSource.getWhereQuery(), + dataRequestId: join.getSourceMetaDataRequestId(), + dynamicStyleProps: this.getCurrentStyle() + .getDynamicPropertiesArray() + .filter((dynamicStyleProp) => { + const matchingField = joinSource.getFieldByName(dynamicStyleProp.getFieldName()); + return ( + dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && + !!matchingField && + dynamicStyleProp.isFieldMetaEnabled() + ); + }), + ...syncContext, + }); + } + + async _syncJoinFormatters(syncContext: DataRequestContext, join: InnerJoin, style: IVectorStyle) { + const joinSource = join.getRightJoinSource(); + return this._syncFormatters({ + source: joinSource, + dataRequestId: join.getSourceFormattersDataRequestId(), + fields: style + .getDynamicPropertiesArray() + .filter((dynamicStyleProp) => { + const matchingField = joinSource.getFieldByName(dynamicStyleProp.getFieldName()); + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && !!matchingField; + }) + .map((dynamicStyleProp) => { + return dynamicStyleProp.getField()!; + }), + ...syncContext, + }); + } + + async _syncSupportsFeatureEditing({ + syncContext, + source, + }: { + syncContext: DataRequestContext; + source: IVectorSource; + }) { + if (syncContext.dataFilters.isReadOnly) { + return; + } + const { startLoading, stopLoading, onLoadError } = syncContext; + const dataRequestId = SUPPORTS_FEATURE_EDITING_REQUEST_ID; + const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); + const prevDataRequest = this.getDataRequest(dataRequestId); + if (prevDataRequest) { + return; + } + try { + startLoading(dataRequestId, requestToken); + const supportsFeatureEditing = await source.supportsFeatureEditing(); + stopLoading(dataRequestId, requestToken, { supportsFeatureEditing }); + } catch (error) { + onLoadError(dataRequestId, requestToken, error.message); + throw error; + } + } + + _getSourceFeatureCollection() { + const sourceDataRequest = this.getSourceDataRequest(); + return sourceDataRequest ? (sourceDataRequest.getData() as FeatureCollection) : null; + } + + _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) { + const prevDataRequest = this.getSourceDataRequest(); + const prevMeta = prevDataRequest?.getMeta(); + if (!prevMeta) { + return true; + } + return source.getUpdateDueToTimeslice(prevMeta, timeslice); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.test.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.test.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.test.ts diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.ts similarity index 99% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.ts index 6afe61f8a16b9..48df2661d269b 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/get_centroid_features.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/get_centroid_features.ts @@ -21,7 +21,7 @@ import turfArea from '@turf/area'; import turfCenterOfMass from '@turf/center-of-mass'; import turfLength from '@turf/length'; import { lineString, polygon } from '@turf/helpers'; -import { GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE } from '../../../../common/constants'; +import { GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE } from '../../../../../common/constants'; export function getCentroidFeatures(featureCollection: FeatureCollection): Feature[] { const centroids = []; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/index.ts new file mode 100644 index 0000000000000..36566ba6c54ab --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/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 { GeoJsonVectorLayer } from './geojson_vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.test.ts similarity index 95% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.test.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.test.ts index 9346bb1621e44..1049c4373c933 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.test.ts @@ -8,16 +8,16 @@ import sinon from 'sinon'; import _ from 'lodash'; import { FeatureCollection } from 'geojson'; -import { ESTermSourceDescriptor } from '../../../../common/descriptor_types'; +import { ESTermSourceDescriptor } from '../../../../../common/descriptor_types'; import { AGG_TYPE, FEATURE_VISIBLE_PROPERTY_NAME, SOURCE_TYPES, -} from '../../../../common/constants'; +} from '../../../../../common/constants'; import { performInnerJoins } from './perform_inner_joins'; -import { InnerJoin } from '../../joins/inner_join'; -import { IVectorSource } from '../../sources/vector_source'; -import { IField } from '../../fields/field'; +import { InnerJoin } from '../../../joins/inner_join'; +import { IVectorSource } from '../../../sources/vector_source'; +import { IField } from '../../../fields/field'; const LEFT_FIELD = 'leftKey'; const COUNT_PROPERTY_NAME = '__kbnjoin__count__d3625663-5b34-4d50-a784-0d743f676a0c'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.ts similarity index 94% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.ts index 23c6527d3e818..3dd2a5ddb377e 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/perform_inner_joins.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/perform_inner_joins.ts @@ -7,10 +7,10 @@ import { FeatureCollection } from 'geojson'; import { i18n } from '@kbn/i18n'; -import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../../common/constants'; -import { DataRequestContext } from '../../../actions'; -import { InnerJoin } from '../../joins/inner_join'; -import { PropertiesMap } from '../../../../common/elasticsearch_util'; +import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../../../common/constants'; +import { DataRequestContext } from '../../../../actions'; +import { InnerJoin } from '../../../joins/inner_join'; +import { PropertiesMap } from '../../../../../common/elasticsearch_util'; interface SourceResult { refreshed: boolean; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx similarity index 92% rename from x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx rename to x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx index cc30f30fe9898..4385adbd4de65 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/utils.tsx @@ -13,19 +13,19 @@ import { SOURCE_BOUNDS_DATA_REQUEST_ID, SOURCE_DATA_REQUEST_ID, VECTOR_SHAPE_TYPE, -} from '../../../../common/constants'; +} from '../../../../../common/constants'; import { DataRequestMeta, MapExtent, Timeslice, VectorSourceRequestMeta, -} from '../../../../common/descriptor_types'; -import { DataRequestContext } from '../../../actions'; -import { IVectorSource } from '../../sources/vector_source'; -import { DataRequestAbortError } from '../../util/data_request'; -import { DataRequest } from '../../util/data_request'; +} from '../../../../../common/descriptor_types'; +import { DataRequestContext } from '../../../../actions'; +import { IVectorSource } from '../../../sources/vector_source'; +import { DataRequestAbortError } from '../../../util/data_request'; +import { DataRequest } from '../../../util/data_request'; import { getCentroidFeatures } from './get_centroid_features'; -import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; +import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; import { assignFeatureIds } from './assign_feature_ids'; export function addGeoJsonMbSource(mbSourceId: string, mbLayerIds: string[], mbMap: MbMap) { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts index 2b14b78f92946..b3d7c47fbc71f 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/index.ts @@ -5,6 +5,14 @@ * 2.0. */ -export { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; +export { + addGeoJsonMbSource, + getVectorSourceBounds, + syncVectorSource, +} from './geojson_vector_layer/utils'; export type { IVectorLayer, VectorLayerArguments } from './vector_layer'; -export { isVectorLayer, VectorLayer, NO_RESULTS_ICON_AND_TOOLTIPCONTENT } from './vector_layer'; +export { isVectorLayer, NO_RESULTS_ICON_AND_TOOLTIPCONTENT } from './vector_layer'; + +export { BlendedVectorLayer } from './blended_vector_layer'; +export { GeoJsonVectorLayer } from './geojson_vector_layer'; +export { MvtVectorLayer } from './mvt_vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap rename to x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/__snapshots__/mvt_vector_layer.test.tsx.snap diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/index.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/index.ts new file mode 100644 index 0000000000000..85ff76f716a7b --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/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 { MvtVectorLayer } from './mvt_vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx similarity index 85% rename from x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx rename to x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx index fd78ea2ebde59..60001cb9e8b1d 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { MockSyncContext } from '../__fixtures__/mock_sync_context'; +import { MockSyncContext } from '../../__fixtures__/mock_sync_context'; import sinon from 'sinon'; import url from 'url'; -jest.mock('../../../kibana_services', () => { +jest.mock('../../../../kibana_services', () => { return { getIsDarkMode() { return false; @@ -20,14 +20,14 @@ jest.mock('../../../kibana_services', () => { import { shallow } from 'enzyme'; import { Feature } from 'geojson'; -import { MVTSingleLayerVectorSource } from '../../sources/mvt_single_layer_vector_source'; +import { MVTSingleLayerVectorSource } from '../../../sources/mvt_single_layer_vector_source'; import { DataRequestDescriptor, TiledSingleLayerVectorSourceDescriptor, VectorLayerDescriptor, -} from '../../../../common/descriptor_types'; -import { SOURCE_TYPES } from '../../../../common/constants'; -import { TiledVectorLayer } from './tiled_vector_layer'; +} from '../../../../../common/descriptor_types'; +import { SOURCE_TYPES } from '../../../../../common/constants'; +import { MvtVectorLayer } from './mvt_vector_layer'; const defaultConfig = { urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', @@ -41,7 +41,7 @@ function createLayer( sourceOptions: Partial = {}, isTimeAware: boolean = false, includeToken: boolean = false -): TiledVectorLayer { +): MvtVectorLayer { const sourceDescriptor: TiledSingleLayerVectorSourceDescriptor = { type: SOURCE_TYPES.MVT_SINGLE_LAYER, ...defaultConfig, @@ -76,28 +76,28 @@ function createLayer( ...layerOptions, sourceDescriptor, }; - const layerDescriptor = TiledVectorLayer.createDescriptor(defaultLayerOptions); - return new TiledVectorLayer({ layerDescriptor, source: mvtSource }); + const layerDescriptor = MvtVectorLayer.createDescriptor(defaultLayerOptions); + return new MvtVectorLayer({ layerDescriptor, source: mvtSource }); } describe('visiblity', () => { it('should get minzoom from source', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); + const layer: MvtVectorLayer = createLayer({}, {}); expect(layer.getMinZoom()).toEqual(4); }); it('should get maxzoom from default', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); + const layer: MvtVectorLayer = createLayer({}, {}); expect(layer.getMaxZoom()).toEqual(24); }); it('should get maxzoom from layer options', async () => { - const layer: TiledVectorLayer = createLayer({ maxZoom: 10 }, {}); + const layer: MvtVectorLayer = createLayer({ maxZoom: 10 }, {}); expect(layer.getMaxZoom()).toEqual(10); }); }); describe('getCustomIconAndTooltipContent', () => { it('Layers with non-elasticsearch sources should display icon', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); + const layer: MvtVectorLayer = createLayer({}, {}); const iconAndTooltipContent = layer.getCustomIconAndTooltipContent(); const component = shallow(iconAndTooltipContent.icon); @@ -107,7 +107,7 @@ describe('getCustomIconAndTooltipContent', () => { describe('getFeatureById', () => { it('should return null feature', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); + const layer: MvtVectorLayer = createLayer({}, {}); const feature = layer.getFeatureById('foobar') as Feature; expect(feature).toEqual(null); }); @@ -115,7 +115,7 @@ describe('getFeatureById', () => { describe('syncData', () => { it('Should sync with source-params', async () => { - const layer: TiledVectorLayer = createLayer({}, {}); + const layer: MvtVectorLayer = createLayer({}, {}); const syncContext = new MockSyncContext({ dataFilters: {} }); @@ -138,7 +138,7 @@ describe('syncData', () => { data: { ...defaultConfig }, dataId: 'source', }; - const layer: TiledVectorLayer = createLayer( + const layer: MvtVectorLayer = createLayer( { __dataRequests: [dataRequestDescriptor], }, @@ -157,7 +157,7 @@ describe('syncData', () => { data: { ...defaultConfig }, dataId: 'source', }; - const layer: TiledVectorLayer = createLayer( + const layer: MvtVectorLayer = createLayer( { __dataRequests: [dataRequestDescriptor], }, @@ -187,7 +187,7 @@ describe('syncData', () => { data: defaultConfig, dataId: 'source', }; - const layer: TiledVectorLayer = createLayer( + const layer: MvtVectorLayer = createLayer( { __dataRequests: [dataRequestDescriptor], }, @@ -217,7 +217,7 @@ describe('syncData', () => { const uuidRegex = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/; it(`should add token in url`, async () => { - const layer: TiledVectorLayer = createLayer({}, {}, false, true); + const layer: MvtVectorLayer = createLayer({}, {}, false, true); const syncContext = new MockSyncContext({ dataFilters: {} }); diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx similarity index 95% rename from x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx rename to x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index 4b881228f79b5..237bab80ce758 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -16,38 +16,33 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { parse as parseUrl } from 'url'; import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; -import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; -import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SOURCE_TYPES } from '../../../../common/constants'; +import { IVectorStyle, VectorStyle } from '../../../styles/vector/vector_style'; +import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SOURCE_TYPES } from '../../../../../common/constants'; import { NO_RESULTS_ICON_AND_TOOLTIPCONTENT, - VectorLayer, + AbstractVectorLayer, VectorLayerArguments, } from '../vector_layer'; -import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source'; -import { DataRequestContext } from '../../../actions'; +import { ITiledSingleLayerVectorSource } from '../../../sources/tiled_single_layer_vector_source'; +import { DataRequestContext } from '../../../../actions'; import { StyleMetaDescriptor, TileMetaFeature, Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, -} from '../../../../common/descriptor_types'; -import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/types'; -import { ESSearchSource } from '../../sources/es_search_source'; -import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; -import { CustomIconAndTooltipContent } from '../layer'; +} from '../../../../../common/descriptor_types'; +import { MVTSingleLayerVectorSourceConfig } from '../../../sources/mvt_single_layer_vector_source/types'; +import { ESSearchSource } from '../../../sources/es_search_source'; +import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; +import { CustomIconAndTooltipContent } from '../../layer'; const ES_MVT_META_LAYER_NAME = 'meta'; const ES_MVT_HITS_TOTAL_RELATION = 'hits.total.relation'; const ES_MVT_HITS_TOTAL_VALUE = 'hits.total.value'; const MAX_RESULT_WINDOW_DATA_REQUEST_ID = 'maxResultWindow'; -/* - * MVT vector layer - */ -export class TiledVectorLayer extends VectorLayer { - static type = LAYER_TYPE.TILED_VECTOR; - +export class MvtVectorLayer extends AbstractVectorLayer { static createDescriptor( descriptor: Partial, mapColors?: string[] diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx index 618be0b21cd73..bd2c8a036bf59 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.test.tsx @@ -27,7 +27,7 @@ import { import { ESTermSourceDescriptor, VectorStyleDescriptor } from '../../../../common/descriptor_types'; import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; import { IVectorSource } from '../../sources/vector_source'; -import { VectorLayer } from './vector_layer'; +import { AbstractVectorLayer } from './vector_layer'; class MockSource { cloneDescriptor() { @@ -64,7 +64,7 @@ describe('cloneDescriptor', () => { }; test('Should update data driven styling properties using join fields', async () => { - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = AbstractVectorLayer.createDescriptor({ style: styleDescriptor, joins: [ { @@ -83,7 +83,7 @@ describe('cloneDescriptor', () => { }, ], }); - const layer = new VectorLayer({ + const layer = new AbstractVectorLayer({ layerDescriptor, source: new MockSource() as unknown as IVectorSource, }); @@ -105,7 +105,7 @@ describe('cloneDescriptor', () => { }); test('Should update data driven styling properties using join fields when metrics are not provided', async () => { - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = AbstractVectorLayer.createDescriptor({ style: styleDescriptor, joins: [ { @@ -120,7 +120,7 @@ describe('cloneDescriptor', () => { }, ], }); - const layer = new VectorLayer({ + const layer = new AbstractVectorLayer({ layerDescriptor, source: new MockSource() as unknown as IVectorSource, }); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 434743ef7ac9e..59078c076433e 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -7,13 +7,9 @@ import React from 'react'; import uuid from 'uuid/v4'; -import type { - Map as MbMap, - AnyLayer as MbLayer, - GeoJSONSource as MbGeoJSONSource, -} from '@kbn/mapbox-gl'; +import type { Map as MbMap, AnyLayer as MbLayer } from '@kbn/mapbox-gl'; import type { Query } from 'src/plugins/data/common'; -import { Feature, FeatureCollection, GeoJsonProperties, Geometry, Position } from 'geojson'; +import { Feature, GeoJsonProperties, Geometry, Position } from 'geojson'; import _ from 'lodash'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -23,24 +19,16 @@ import { AGG_TYPE, SOURCE_META_DATA_REQUEST_ID, SOURCE_FORMATTERS_DATA_REQUEST_ID, - FEATURE_VISIBLE_PROPERTY_NAME, - EMPTY_FEATURE_COLLECTION, LAYER_TYPE, FIELD_ORIGIN, FieldFormatter, SOURCE_TYPES, STYLE_TYPE, - SUPPORTS_FEATURE_EDITING_REQUEST_ID, VECTOR_STYLES, } from '../../../../common/constants'; import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; import { DataRequestAbortError } from '../../util/data_request'; -import { - canSkipSourceUpdate, - canSkipStyleMetaUpdate, - canSkipFormattersUpdate, -} from '../../util/can_skip_fetch'; -import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds'; +import { canSkipStyleMetaUpdate, canSkipFormattersUpdate } from '../../util/can_skip_fetch'; import { getLabelFilterExpression, getFillFilterExpression, @@ -55,13 +43,10 @@ import { ESTermSourceDescriptor, JoinDescriptor, StyleMetaDescriptor, - Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStyleRequestMeta, - VectorJoinSourceRequestMeta, } from '../../../../common/descriptor_types'; -import { ISource } from '../../sources/source'; import { IVectorSource } from '../../sources/vector_source'; import { CustomIconAndTooltipContent, ILayer } from '../layer'; import { InnerJoin } from '../../joins/inner_join'; @@ -70,13 +55,10 @@ import { DataRequestContext } from '../../../actions'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; import { IESSource } from '../../sources/es_source'; -import { PropertiesMap } from '../../../../common/elasticsearch_util'; import { ITermJoinSource } from '../../sources/term_join_source'; -import { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './utils'; -import { JoinState, performInnerJoins } from './perform_inner_joins'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { getJoinAggKey } from '../../../../common/get_agg_key'; -import { GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids'; +import { getVectorSourceBounds } from './geojson_vector_layer/utils'; export function isVectorLayer(layer: ILayer) { return (layer as IVectorLayer).canShowTooltip !== undefined; @@ -114,7 +96,7 @@ export interface IVectorLayer extends ILayer { deleteFeature(featureId: string): Promise; } -const noResultsIcon = ; +export const noResultsIcon = ; export const NO_RESULTS_ICON_AND_TOOLTIPCONTENT = { icon: noResultsIcon, tooltipContent: i18n.translate('xpack.maps.vectorLayer.noResultsFoundTooltip', { @@ -122,12 +104,7 @@ export const NO_RESULTS_ICON_AND_TOOLTIPCONTENT = { }), }; -/* - * Geojson vector layer - */ -export class VectorLayer extends AbstractLayer implements IVectorLayer { - static type = LAYER_TYPE.VECTOR; - +export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { protected readonly _style: VectorStyle; private readonly _joins: InnerJoin[]; @@ -265,9 +242,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } supportsFeatureEditing(): boolean { - const dataRequest = this.getDataRequest(SUPPORTS_FEATURE_EDITING_REQUEST_ID); - const data = dataRequest?.getData() as { supportsFeatureEditing: boolean } | undefined; - return data ? data.supportsFeatureEditing : false; + return false; } hasJoins() { @@ -296,38 +271,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { - const featureCollection = this._getSourceFeatureCollection(); - - if (!featureCollection || featureCollection.features.length === 0) { - return NO_RESULTS_ICON_AND_TOOLTIPCONTENT; - } - - if ( - this.getJoins().length && - !featureCollection.features.some( - (feature) => feature.properties?.[FEATURE_VISIBLE_PROPERTY_NAME] - ) - ) { - return { - icon: noResultsIcon, - tooltipContent: i18n.translate('xpack.maps.vectorLayer.noResultsFoundInJoinTooltip', { - defaultMessage: `No matching results found in term joins`, - }), - }; - } - - const sourceDataRequest = this.getSourceDataRequest(); - const { tooltipContent, areResultsTrimmed, isDeprecated } = - this.getSource().getSourceTooltipContent(sourceDataRequest); - return { - icon: isDeprecated ? ( - - ) : ( - this.getCurrentStyle().getIcon() - ), - tooltipContent, - areResultsTrimmed, - }; + throw new Error('Should implement AbstractVectorLayer#getCustomIconAndTooltipContent'); } getLayerTypeIconName() { @@ -343,15 +287,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } async getBounds(syncContext: DataRequestContext) { - const isStaticLayer = !this.getSource().isBoundsAware(); - return isStaticLayer || this.hasJoins() - ? getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()) - : getVectorSourceBounds({ - layerId: this.getId(), - syncContext, - source: this.getSource(), - sourceQuery: this.getQuery(), - }); + return getVectorSourceBounds({ + layerId: this.getId(), + syncContext, + source: this.getSource(), + sourceQuery: this.getQuery(), + }); } async getLeftJoinFields() { @@ -409,79 +350,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - async _syncJoin({ - join, - startLoading, - stopLoading, - onLoadError, - registerCancelCallback, - dataFilters, - isForceRefresh, - }: { join: InnerJoin } & DataRequestContext): Promise { - const joinSource = join.getRightJoinSource(); - const sourceDataId = join.getSourceDataRequestId(); - const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); - - const joinRequestMeta: VectorJoinSourceRequestMeta = buildVectorRequestMeta( - joinSource, - joinSource.getFieldNames(), - dataFilters, - joinSource.getWhereQuery(), - isForceRefresh - ) as VectorJoinSourceRequestMeta; - - const prevDataRequest = this.getDataRequest(sourceDataId); - const canSkipFetch = await canSkipSourceUpdate({ - source: joinSource, - prevDataRequest, - nextRequestMeta: joinRequestMeta, - extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource). - getUpdateDueToTimeslice: () => { - return true; - }, - }); - - if (canSkipFetch) { - return { - dataHasChanged: false, - join, - propertiesMap: prevDataRequest?.getData() as PropertiesMap, - }; - } - - try { - startLoading(sourceDataId, requestToken, joinRequestMeta); - const leftSourceName = await this._source.getDisplayName(); - const propertiesMap = await joinSource.getPropertiesMap( - joinRequestMeta, - leftSourceName, - join.getLeftField().getName(), - registerCancelCallback.bind(null, requestToken) - ); - stopLoading(sourceDataId, requestToken, propertiesMap); - return { - dataHasChanged: true, - join, - propertiesMap, - }; - } catch (error) { - if (!(error instanceof DataRequestAbortError)) { - onLoadError(sourceDataId, requestToken, `Join error: ${error.message}`); - } - throw error; - } - } - - async _syncJoins(syncContext: DataRequestContext, style: IVectorStyle) { - const joinSyncs = this.getValidJoins().map(async (join) => { - await this._syncJoinStyleMeta(syncContext, join, style); - await this._syncJoinFormatters(syncContext, join, style); - return this._syncJoin({ join, ...syncContext }); - }); - - return await Promise.all(joinSyncs); - } - async _getVectorSourceRequestMeta( isForceRefresh: boolean, dataFilters: DataFilters, @@ -522,27 +390,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - async _syncJoinStyleMeta(syncContext: DataRequestContext, join: InnerJoin, style: IVectorStyle) { - const joinSource = join.getRightJoinSource(); - return this._syncStyleMeta({ - source: joinSource, - style, - sourceQuery: joinSource.getWhereQuery(), - dataRequestId: join.getSourceMetaDataRequestId(), - dynamicStyleProps: this.getCurrentStyle() - .getDynamicPropertiesArray() - .filter((dynamicStyleProp) => { - const matchingField = joinSource.getFieldByName(dynamicStyleProp.getFieldName()); - return ( - dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && - !!matchingField && - dynamicStyleProp.isFieldMetaEnabled() - ); - }), - ...syncContext, - }); - } - async _syncStyleMeta({ source, style, @@ -626,24 +473,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - async _syncJoinFormatters(syncContext: DataRequestContext, join: InnerJoin, style: IVectorStyle) { - const joinSource = join.getRightJoinSource(); - return this._syncFormatters({ - source: joinSource, - dataRequestId: join.getSourceFormattersDataRequestId(), - fields: style - .getDynamicPropertiesArray() - .filter((dynamicStyleProp) => { - const matchingField = joinSource.getFieldByName(dynamicStyleProp.getFieldName()); - return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && !!matchingField; - }) - .map((dynamicStyleProp) => { - return dynamicStyleProp.getField()!; - }), - ...syncContext, - }); - } - async _syncFormatters({ source, dataRequestId, @@ -693,128 +522,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } } - async syncData(syncContext: DataRequestContext) { - await this._syncData(syncContext, this.getSource(), this.getCurrentStyle()); - } - - // TLDR: Do not call getSource or getCurrentStyle in syncData flow. Use 'source' and 'style' arguments instead. - // - // 1) State is contained in the redux store. Layer instance state is readonly. - // 2) Even though data request descriptor updates trigger new instances for rendering, - // syncing data executes on a single object instance. Syncing data can not use updated redux store state. - // - // Blended layer data syncing branches on the source/style depending on whether clustering is used or not. - // Given 1 above, which source/style to use can not be stored in Layer instance state. - // Given 2 above, which source/style to use can not be pulled from data request state. - // Therefore, source and style are provided as arugments and must be used instead of calling getSource or getCurrentStyle. - async _syncData(syncContext: DataRequestContext, source: IVectorSource, style: IVectorStyle) { - if (this.isLoadingBounds()) { - return; - } - - try { - await this._syncSourceStyleMeta(syncContext, source, style); - await this._syncSourceFormatters(syncContext, source, style); - const sourceResult = await syncVectorSource({ - layerId: this.getId(), - layerName: await this.getDisplayName(source), - prevDataRequest: this.getSourceDataRequest(), - requestMeta: await this._getVectorSourceRequestMeta( - syncContext.isForceRefresh, - syncContext.dataFilters, - source, - style - ), - syncContext, - source, - getUpdateDueToTimeslice: (timeslice?: Timeslice) => { - return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); - }, - }); - await this._syncSupportsFeatureEditing({ syncContext, source }); - if ( - !sourceResult.featureCollection || - !sourceResult.featureCollection.features.length || - !this.hasJoins() - ) { - return; - } - - const joinStates = await this._syncJoins(syncContext, style); - performInnerJoins( - sourceResult, - joinStates, - syncContext.updateSourceData, - syncContext.onJoinError - ); - } catch (error) { - if (!(error instanceof DataRequestAbortError)) { - throw error; - } - } - } - - async _syncSupportsFeatureEditing({ - syncContext, - source, - }: { - syncContext: DataRequestContext; - source: IVectorSource; - }) { - if (syncContext.dataFilters.isReadOnly) { - return; - } - const { startLoading, stopLoading, onLoadError } = syncContext; - const dataRequestId = SUPPORTS_FEATURE_EDITING_REQUEST_ID; - const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); - const prevDataRequest = this.getDataRequest(dataRequestId); - if (prevDataRequest) { - return; - } - try { - startLoading(dataRequestId, requestToken); - const supportsFeatureEditing = await source.supportsFeatureEditing(); - stopLoading(dataRequestId, requestToken, { supportsFeatureEditing }); - } catch (error) { - onLoadError(dataRequestId, requestToken, error.message); - throw error; - } - } - - _getSourceFeatureCollection() { - if (this.getSource().isMvt()) { - return null; - } - const sourceDataRequest = this.getSourceDataRequest(); - return sourceDataRequest ? (sourceDataRequest.getData() as FeatureCollection) : null; - } - - _syncFeatureCollectionWithMb(mbMap: MbMap) { - const mbGeoJSONSource = mbMap.getSource(this.getId()) as MbGeoJSONSource; - const featureCollection = this._getSourceFeatureCollection(); - const featureCollectionOnMap = AbstractLayer.getBoundDataForSource(mbMap, this.getId()); - - if (!featureCollection) { - if (featureCollectionOnMap) { - this.getCurrentStyle().clearFeatureState(featureCollectionOnMap, mbMap, this.getId()); - } - mbGeoJSONSource.setData(EMPTY_FEATURE_COLLECTION); - return; - } - - // "feature-state" data expressions are not supported with layout properties. - // To work around this limitation, - // scaled layout properties (like icon-size) must fall back to geojson property values :( - const hasGeoJsonProperties = this.getCurrentStyle().setFeatureStateAndStyleProps( - featureCollection, - mbMap, - this.getId() - ); - if (featureCollection !== featureCollectionOnMap || hasGeoJsonProperties) { - mbGeoJSONSource.setData(featureCollection); - } - } - _setMbPointsProperties( mbMap: MbMap, mvtSourceLayer?: string, @@ -989,33 +696,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayerZoomRange(labelLayerId, this.getMinZoom(), this.getMaxZoom()); } - _syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) { - const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice); - this._setMbLabelProperties(mbMap, undefined, timesliceMaskConfig); - this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig); - this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig); - } - - _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined { - if (!timeslice || this.hasJoins()) { - return; - } - - const prevMeta = this.getSourceDataRequest()?.getMeta(); - return prevMeta !== undefined && prevMeta.timesliceMaskField !== undefined - ? { - timesliceMaskField: prevMeta.timesliceMaskField, - timeslice, - } - : undefined; - } - - syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) { - addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); - this._syncFeatureCollectionWithMb(mbMap); - this._syncStylePropertiesWithMb(mbMap, timeslice); - } - _getMbPointLayerId() { return this.makeMbLayerId('circle'); } @@ -1090,34 +770,17 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } getFeatureId(feature: Feature): string | number | undefined { - return feature.properties?.[GEOJSON_FEATURE_ID_PROPERTY_NAME]; + throw new Error('Should implement AbstractVectorLayer#getFeatureId'); } - getFeatureById(id: string | number) { - const featureCollection = this._getSourceFeatureCollection(); - if (!featureCollection) { - return null; - } - - const targetFeature = featureCollection.features.find((feature) => { - return this.getFeatureId(feature) === id; - }); - return targetFeature ? targetFeature : null; + getFeatureById(id: string | number): Feature | null { + throw new Error('Should implement AbstractVectorLayer#getFeatureById'); } async getLicensedFeatures() { return await this._source.getLicensedFeatures(); } - _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) { - const prevDataRequest = this.getSourceDataRequest(); - const prevMeta = prevDataRequest?.getMeta(); - if (!prevMeta) { - return true; - } - return source.getUpdateDueToTimeslice(prevMeta, timeslice); - } - async addFeature(geometry: Geometry | Position[]) { const layerSource = this.getSource(); const defaultFields = await layerSource.getDefaultFields(); @@ -1130,11 +793,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } async getStyleMetaDescriptorFromLocalFeatures(): Promise { - const sourceDataRequest = this.getSourceDataRequest(); - const style = this.getCurrentStyle(); - if (!style || !sourceDataRequest) { - return null; - } - return await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); + throw new Error('Should implement AbstractVectorLayer#getStyleMetaDescriptorFromLocalFeatures'); } } diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index d4cf4dbee7943..dd2317506e5f9 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { EMSFileSource, getSourceTitle } from './ems_file_source'; @@ -46,7 +46,7 @@ export const emsBoundariesLayerWizardConfig: LayerWizard = { renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); - const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index 36dd28cb5bbf1..ad046eeb02d47 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { CreateSourceEditor } from './create_source_editor'; import { ESGeoGridSource, clustersTitle } from './es_geo_grid_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { ESGeoGridSourceDescriptor, ColorDynamicOptions, @@ -45,7 +45,7 @@ export const clustersLayerWizardConfig: LayerWizard = { } const defaultDynamicProperties = getDefaultDynamicProperties(); - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig), style: VectorStyle.createDescriptor({ // @ts-ignore diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx index 8da7037a5a34c..ba1c3c4eece4b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/layer_wizard.tsx @@ -12,7 +12,7 @@ import { ESGeoLineSource, geoLineTitle, REQUIRES_GOLD_LICENSE_MSG } from './es_g import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { LAYER_WIZARD_CATEGORY, STYLE_TYPE, VECTOR_STYLES } from '../../../../common/constants'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { getIsGoldPlus } from '../../../licensed_features'; import { TracksLayerIcon } from '../../layers/icons/tracks_layer_icon'; @@ -40,7 +40,7 @@ export const geoLineLayerWizardConfig: LayerWizard = { return; } - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor: ESGeoLineSource.createDescriptor(sourceConfig), style: VectorStyle.createDescriptor({ [VECTOR_STYLES.LINE_WIDTH]: { diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index c94c7859a85e7..84dea15daf48f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; // @ts-ignore import { ESPewPewSource, sourceTitle } from './es_pew_pew_source'; import { VectorStyle } from '../../styles/vector/vector_style'; @@ -40,7 +40,7 @@ export const point2PointLayerWizardConfig: LayerWizard = { } const defaultDynamicProperties = getDefaultDynamicProperties(); - const layerDescriptor = VectorLayer.createDescriptor({ + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor: ESPewPewSource.createDescriptor(sourceConfig), style: VectorStyle.createDescriptor({ [VECTOR_STYLES.LINE_COLOR]: { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts index 41b4e8d7a318a..5553e925258e9 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts @@ -9,7 +9,7 @@ import { Query } from 'src/plugins/data/public'; import { LayerDescriptor } from '../../../../common/descriptor_types'; import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants'; import { ESSearchSource } from './es_search_source'; -import { VectorLayer } from '../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../layers/vector_layer'; import { getIsGoldPlus } from '../../../licensed_features'; export interface CreateLayerDescriptorParams { @@ -37,5 +37,5 @@ export function createLayerDescriptor({ scalingType, }); - return VectorLayer.createDescriptor({ sourceDescriptor, query }); + return GeoJsonVectorLayer.createDescriptor({ sourceDescriptor, query }); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 26771c1bed023..601fcee50ab2a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -11,10 +11,8 @@ import React from 'react'; import { CreateSourceEditor } from './create_source_editor'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { ESSearchSource, sourceTitle } from './es_search_source'; -import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer'; -import { VectorLayer } from '../../layers/vector_layer'; +import { BlendedVectorLayer, GeoJsonVectorLayer, MvtVectorLayer } from '../../layers/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; -import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; import { DocumentsLayerIcon } from '../../layers/icons/documents_layer_icon'; import { ESSearchSourceDescriptor, @@ -30,9 +28,9 @@ export function createDefaultLayerDescriptor( if (sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS) { return BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); } else if (sourceDescriptor.scalingType === SCALING_TYPES.MVT) { - return TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + return MvtVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); } else { - return VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + return GeoJsonVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx index e02ada305ecff..b4339eb20b1fd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { CreateSourceEditor } from './create_source_editor'; import { LayerWizard, RenderWizardArguments } from '../../../layers/layer_wizard_registry'; -import { VectorLayer } from '../../../layers/vector_layer'; +import { GeoJsonVectorLayer } from '../../../layers/vector_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../../common/constants'; import { TopHitsLayerIcon } from '../../../layers/icons/top_hits_layer_icon'; import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types'; @@ -30,7 +30,7 @@ export const esTopHitsLayerWizardConfig: LayerWizard = { } const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); - const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + const layerDescriptor = GeoJsonVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index e5e1877c1ccd1..a3f7ceafd54ef 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor'; import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; +import { MvtVectorLayer } from '../../layers/vector_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types'; import { VectorTileLayerIcon } from '../../layers/icons/vector_tile_layer_icon'; @@ -24,7 +24,7 @@ export const mvtVectorSourceWizardConfig: LayerWizard = { renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => { const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); - const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + const layerDescriptor = MvtVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); previewLayers([layerDescriptor]); }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 93dfebecd1c34..eb0196ea156aa 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -50,7 +50,7 @@ import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { TileStatusTracker } from './tile_status_tracker'; import { DrawFeatureControl } from './draw_control/draw_feature_control'; -import { TiledVectorLayer } from '../../classes/layers/tiled_vector_layer/tiled_vector_layer'; +import { MvtVectorLayer } from '../../classes/layers/vector_layer'; import type { MapExtentState } from '../../reducers/map/types'; export interface Props { @@ -127,7 +127,7 @@ export class MbMap extends Component { // This keeps track of the latest update calls, per layerId _queryForMeta = (layer: ILayer) => { if (this.state.mbMap && layer.isVisible() && layer.getType() === LAYER_TYPE.TILED_VECTOR) { - const mbFeatures = (layer as TiledVectorLayer).queryTileMetaFeatures(this.state.mbMap); + const mbFeatures = (layer as MvtVectorLayer).queryTileMetaFeatures(this.state.mbMap); if (mbFeatures !== null) { this.props.updateMetaFromTiles(layer.getId(), mbFeatures); } 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 51acab6453921..db903c6a02593 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 @@ -18,7 +18,7 @@ import { getVisibilityToggleLabel, } from '../action_labels'; import { ESSearchSource } from '../../../../../../classes/sources/es_search_source'; -import { VectorLayer } from '../../../../../../classes/layers/vector_layer'; +import { isVectorLayer, IVectorLayer } from '../../../../../../classes/layers/vector_layer'; import { SCALING_TYPES, VECTOR_SHAPE_TYPE } from '../../../../../../../common/constants'; export interface Props { @@ -67,10 +67,10 @@ export class TOCEntryActionsPopover extends Component { } async _loadFeatureEditing() { - if (!(this.props.layer instanceof VectorLayer)) { + if (!isVectorLayer(this.props.layer)) { return; } - const supportsFeatureEditing = this.props.layer.supportsFeatureEditing(); + const supportsFeatureEditing = (this.props.layer as IVectorLayer).supportsFeatureEditing(); const isFeatureEditingEnabled = await this._getIsFeatureEditingEnabled(); if ( !this._isMounted || @@ -83,7 +83,7 @@ export class TOCEntryActionsPopover extends Component { } async _getIsFeatureEditingEnabled(): Promise { - const vectorLayer = this.props.layer as VectorLayer; + const vectorLayer = this.props.layer as IVectorLayer; const layerSource = this.props.layer.getSource(); if (!(layerSource instanceof ESSearchSource)) { return false; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index dc3c6dca46237..4f336d9a8ad27 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -7,8 +7,6 @@ import { LAYER_STYLE_TYPE, LAYER_TYPE, SOURCE_TYPES } from '../../common/constants'; -jest.mock('../classes/layers/tiled_vector_layer/tiled_vector_layer', () => {}); -jest.mock('../classes/layers/blended_vector_layer/blended_vector_layer', () => {}); jest.mock('../classes/layers/heatmap_layer', () => {}); jest.mock('../classes/layers/vector_tile_layer/vector_tile_layer', () => {}); jest.mock('../classes/joins/inner_join', () => {}); diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 5ca297bdff020..f58525ea6f974 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -13,21 +13,25 @@ import type { Query } from 'src/plugins/data/common'; import { TileLayer } from '../classes/layers/tile_layer/tile_layer'; // @ts-ignore import { VectorTileLayer } from '../classes/layers/vector_tile_layer/vector_tile_layer'; -import { IVectorLayer, VectorLayer } from '../classes/layers/vector_layer'; +import { + BlendedVectorLayer, + IVectorLayer, + MvtVectorLayer, + GeoJsonVectorLayer, +} from '../classes/layers/vector_layer'; import { VectorStyle } from '../classes/styles/vector/vector_style'; import { HeatmapLayer } from '../classes/layers/heatmap_layer'; -import { BlendedVectorLayer } from '../classes/layers/blended_vector_layer/blended_vector_layer'; import { getTimeFilter } from '../kibana_services'; import { getChartsPaletteServiceGetColor, getInspectorAdapters, } from '../reducers/non_serializable_instances'; -import { TiledVectorLayer } from '../classes/layers/tiled_vector_layer/tiled_vector_layer'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/copy_persistent_state'; import { InnerJoin } from '../classes/joins/inner_join'; import { getSourceByType } from '../classes/sources/source_registry'; import { GeoJsonFileSource } from '../classes/sources/geojson_file_source'; import { + LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SPATIAL_FILTERS_LAYER_ID, STYLE_TYPE, @@ -66,9 +70,9 @@ export function createLayerInstance( const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); switch (layerDescriptor.type) { - case TileLayer.type: + case LAYER_TYPE.TILE: return new TileLayer({ layerDescriptor, source: source as ITMSSource }); - case VectorLayer.type: + case LAYER_TYPE.VECTOR: const joins: InnerJoin[] = []; const vectorLayerDescriptor = layerDescriptor as VectorLayerDescriptor; if (vectorLayerDescriptor.joins) { @@ -77,27 +81,27 @@ export function createLayerInstance( joins.push(join); }); } - return new VectorLayer({ + return new GeoJsonVectorLayer({ layerDescriptor: vectorLayerDescriptor, source: source as IVectorSource, joins, chartsPaletteServiceGetColor, }); - case VectorTileLayer.type: + case LAYER_TYPE.VECTOR_TILE: return new VectorTileLayer({ layerDescriptor, source: source as ITMSSource }); - case HeatmapLayer.type: + case LAYER_TYPE.HEATMAP: return new HeatmapLayer({ layerDescriptor: layerDescriptor as HeatmapLayerDescriptor, source: source as ESGeoGridSource, }); - case BlendedVectorLayer.type: + case LAYER_TYPE.BLENDED_VECTOR: return new BlendedVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, chartsPaletteServiceGetColor, }); - case TiledVectorLayer.type: - return new TiledVectorLayer({ + case LAYER_TYPE.TILED_VECTOR: + return new MvtVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, }); @@ -266,8 +270,8 @@ export const getSpatialFiltersLayer = createSelector( name: 'spatialFilters', }); - return new VectorLayer({ - layerDescriptor: VectorLayer.createDescriptor({ + return new GeoJsonVectorLayer({ + layerDescriptor: GeoJsonVectorLayer.createDescriptor({ id: SPATIAL_FILTERS_LAYER_ID, visible: settings.showSpatialFilters, alpha: settings.spatialFiltersAlpa, From 1b82502dbbd725db9857f121910f73256e7bd3d9 Mon Sep 17 00:00:00 2001 From: "Lucas F. da Costa" Date: Mon, 8 Nov 2021 19:10:59 +0000 Subject: [PATCH 08/98] [User Experience] Add error boundary to prevent UX dashboard from crashing the application (#117583) * wrap UX dashboard into an error boundary (fixes #117543) * refactor APM root app tests to reuse coreMock Before this change, the tests for the root application component of the APM app were manually mocking the `coreStart` objects required to render the component. After this change, these tests will now reuse the relevant `coreMock` methods. * refactor: fix typo on createAppMountParameters test utility Co-authored-by: Lucas Fernandes da Costa Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/tutorials/testing_plugins.mdx | 8 +- src/core/public/mocks.ts | 2 +- .../public/application/application.test.tsx | 72 +++++++++---- .../plugins/apm/public/application/uxApp.tsx | 5 +- .../apm_plugin/mock_apm_plugin_context.tsx | 101 +++++++----------- .../public/applications/index.test.tsx | 2 +- .../capture_url/capture_url_app.test.ts | 4 +- 7 files changed, 105 insertions(+), 89 deletions(-) diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index b8f8029e7b16c..044d610aa3489 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -458,7 +458,7 @@ describe('Plugin', () => { const [coreStartMock, startDepsMock] = await coreSetup.getStartServices(); const unmountMock = jest.fn(); renderAppMock.mockReturnValue(unmountMock); - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup); // Grab registered mount function @@ -528,7 +528,7 @@ import { renderApp } from './application'; describe('renderApp', () => { it('mounts and unmounts UI', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Verify some expected DOM element is rendered into the element @@ -540,7 +540,7 @@ describe('renderApp', () => { }); it('unsubscribes from uiSettings', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Create a fake Subject you can use to monitor observers const settings$ = new Subject(); @@ -555,7 +555,7 @@ describe('renderApp', () => { }); it('resets chrome visibility', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Verify stateful Core API was called on mount diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index bd7623beba651..39d2dc3d5c497 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -169,5 +169,5 @@ export const coreMock = { createStart: createCoreStartMock, createPluginInitializerContext: pluginInitializerContextMock, createStorage: createStorageMock, - createAppMountParamters: createAppMountParametersMock, + createAppMountParameters: createAppMountParametersMock, }; diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index 56e7b4684acde..12170ac20b7df 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -7,24 +7,31 @@ import React from 'react'; import { act } from '@testing-library/react'; +import { EuiErrorBoundary } from '@elastic/eui'; +import { mount } from 'enzyme'; import { createMemoryHistory } from 'history'; import { Observable } from 'rxjs'; -import { CoreStart, DocLinksStart, HttpStart } from 'src/core/public'; +import { AppMountParameters, DocLinksStart, HttpStart } from 'src/core/public'; import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context'; import { createCallApmApi } from '../services/rest/createCallApmApi'; -import { renderApp } from './'; +import { renderApp as renderApmApp } from './'; +import { UXAppRoot } from './uxApp'; import { disableConsoleWarning } from '../utils/testHelpers'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ApmPluginStartDeps } from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; +import { RumHome } from '../components/app/RumDashboard/RumHome'; jest.mock('../services/rest/data_view', () => ({ createStaticDataView: () => Promise.resolve(undefined), })); -describe('renderApp', () => { - let mockConsole: jest.SpyInstance; +jest.mock('../components/app/RumDashboard/RumHome', () => ({ + RumHome: () =>

Home Mock

, +})); +describe('renderApp (APM)', () => { + let mockConsole: jest.SpyInstance; beforeAll(() => { // The RUM agent logs an unnecessary message here. There's a couple open // issues need to be fixed to get the ability to turn off all of the logging: @@ -40,11 +47,15 @@ describe('renderApp', () => { mockConsole.mockRestore(); }); - it('renders the app', () => { - const { core, config, observabilityRuleTypeRegistry } = - mockApmPluginContextValue; + const getApmMountProps = () => { + const { + core: coreStart, + config, + observabilityRuleTypeRegistry, + corePlugins, + } = mockApmPluginContextValue; - const plugins = { + const pluginsSetup = { licensing: { license$: new Observable() }, triggersActionsUi: { actionTypeRegistry: {}, ruleTypeRegistry: {} }, data: { @@ -99,7 +110,7 @@ describe('renderApp', () => { } as unknown as ApmPluginStartDeps; jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined); - createCallApmApi(core as unknown as CoreStart); + createCallApmApi(coreStart); jest .spyOn(window.console, 'warn') @@ -111,17 +122,24 @@ describe('renderApp', () => { } }); + return { + coreStart, + pluginsSetup: pluginsSetup as unknown as ApmPluginSetupDeps, + appMountParameters: appMountParameters as unknown as AppMountParameters, + pluginsStart, + config, + observabilityRuleTypeRegistry, + corePlugins, + }; + }; + + it('renders the app', () => { + const mountProps = getApmMountProps(); + let unmount: () => void; act(() => { - unmount = renderApp({ - coreStart: core as any, - pluginsSetup: plugins as any, - appMountParameters: appMountParameters as any, - pluginsStart, - config, - observabilityRuleTypeRegistry, - }); + unmount = renderApmApp(mountProps); }); expect(() => { @@ -129,3 +147,21 @@ describe('renderApp', () => { }).not.toThrowError(); }); }); + +describe('renderUxApp', () => { + it('has an error boundary for the UXAppRoot', async () => { + const uxMountProps = mockApmPluginContextValue; + + const wrapper = mount(); + + wrapper + .find(RumHome) + .simulateError(new Error('Oh no, an unexpected error!')); + + expect(wrapper.find(RumHome)).toHaveLength(0); + expect(wrapper.find(EuiErrorBoundary)).toHaveLength(1); + expect(wrapper.find(EuiErrorBoundary).text()).toMatch( + /Error: Oh no, an unexpected error!/ + ); + }); +}); diff --git a/x-pack/plugins/apm/public/application/uxApp.tsx b/x-pack/plugins/apm/public/application/uxApp.tsx index 2e4ba786811f8..51ce192327043 100644 --- a/x-pack/plugins/apm/public/application/uxApp.tsx +++ b/x-pack/plugins/apm/public/application/uxApp.tsx @@ -7,6 +7,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiErrorBoundary } from '@elastic/eui'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -133,7 +134,9 @@ export function UXAppRoot({ - + + + diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index abdab939f4a0a..b519388a8bac7 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -6,11 +6,11 @@ */ import React, { ReactNode, useMemo } from 'react'; -import { Observable, of } from 'rxjs'; import { RouterProvider } from '@kbn/typed-react-router-config'; import { useHistory } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { merge } from 'lodash'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlService } from '../../../../../../src/plugins/share/common/url_service'; import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public'; import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context'; @@ -20,72 +20,43 @@ import { createCallApmApi } from '../../services/rest/createCallApmApi'; import { apmRouter } from '../../components/routing/apm_route_config'; import { MlLocatorDefinition } from '../../../../ml/public'; -const uiSettings: Record = { - [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - }, - ], - [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { - from: 'now-15m', - to: 'now', - }, - [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { - pause: false, - value: 100000, - }, -}; +const coreStart = coreMock.createStart({ basePath: '/basepath' }); -const mockCore = { +const mockCore = merge({}, coreStart, { application: { capabilities: { apm: {}, ml: {}, }, - currentAppId$: new Observable(), - getUrlForApp: (appId: string) => '', - navigateToUrl: (url: string) => {}, - }, - chrome: { - docTitle: { change: () => {} }, - setBreadcrumbs: () => {}, - setHelpExtension: () => {}, - setBadge: () => {}, - }, - docLinks: { - DOC_LINK_VERSION: '0', - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - links: { - apm: {}, - }, - }, - http: { - basePath: { - prepend: (path: string) => `/basepath${path}`, - get: () => `/basepath`, - }, - }, - i18n: { - Context: ({ children }: { children: ReactNode }) => children, - }, - notifications: { - toasts: { - addWarning: () => {}, - addDanger: () => {}, - }, }, uiSettings: { - get: (key: string) => uiSettings[key], - get$: (key: string) => of(mockCore.uiSettings.get(key)), + get: (key: string) => { + const uiSettings: Record = { + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + ], + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + from: 'now-15m', + to: 'now', + }, + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + pause: false, + value: 100000, + }, + }; + return uiSettings[key]; + }, }, -}; +}); const mockConfig: ConfigSchema = { serviceMapEnabled: true, @@ -118,16 +89,22 @@ const mockPlugin = { }, }; -const mockAppMountParameters = { - setHeaderActionMenu: () => {}, +const mockCorePlugins = { + embeddable: {}, + inspector: {}, + maps: {}, + observability: {}, + data: {}, }; export const mockApmPluginContextValue = { - appMountParameters: mockAppMountParameters, + appMountParameters: coreMock.createAppMountParameters('/basepath'), config: mockConfig, core: mockCore, plugins: mockPlugin, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + corePlugins: mockCorePlugins, + deps: {}, }; export function MockApmPluginContextWrapper({ @@ -135,7 +112,7 @@ export function MockApmPluginContextWrapper({ value = {} as ApmPluginContextValue, history, }: { - children?: React.ReactNode; + children?: ReactNode; value?: ApmPluginContextValue; history?: History; }) { diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 5a82f15a6cf04..356a3c26b910e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -23,7 +23,7 @@ import { renderApp, renderHeaderActions } from './'; describe('renderApp', () => { const kibanaDeps = { - params: coreMock.createAppMountParamters(), + params: coreMock.createAppMountParameters(), core: coreMock.createStart(), plugins: { licensing: licensingMock.createStart(), diff --git a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts index 44fd5ab195341..0283df88fdb87 100644 --- a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts +++ b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts @@ -59,7 +59,7 @@ describe('captureURLApp', () => { captureURLApp.create(coreSetupMock); const [[{ mount }]] = coreSetupMock.application.register.mock.calls; - await mount(coreMock.createAppMountParamters()); + await mount(coreMock.createAppMountParameters()); expect(mockLocationReplace).toHaveBeenCalledTimes(1); expect(mockLocationReplace).toHaveBeenCalledWith( @@ -77,7 +77,7 @@ describe('captureURLApp', () => { captureURLApp.create(coreSetupMock); const [[{ mount }]] = coreSetupMock.application.register.mock.calls; - await mount(coreMock.createAppMountParamters()); + await mount(coreMock.createAppMountParameters()); expect(mockLocationReplace).toHaveBeenCalledTimes(1); expect(mockLocationReplace).toHaveBeenCalledWith( From 30493f90be086717ae3a74b0a00d082da52480c3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Nov 2021 19:13:54 +0000 Subject: [PATCH 09/98] chore(NA): removes invalid ui_actions docs links (#117877) --- src/plugins/ui_actions/README.asciidoc | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/plugins/ui_actions/README.asciidoc b/src/plugins/ui_actions/README.asciidoc index 8fd3d0378ce6e..b9e25edde77e6 100644 --- a/src/plugins/ui_actions/README.asciidoc +++ b/src/plugins/ui_actions/README.asciidoc @@ -71,13 +71,3 @@ action to execute. https://github.com/elastic/kibana/blob/main/examples/ui_action_examples/README.md[ui_action examples] -=== API Docs - -==== Server API -https://github.com/elastic/kibana/blob/main/docs/development/plugins/ui_actions/server/kibana-plugin-plugins-ui_actions-server.uiactionssetup.md[Browser Setup contract] -https://github.com/elastic/kibana/blob/main/docs/development/plugins/ui_actions/server/kibana-plugin-plugins-ui_actions-server.uiactionsstart.md[Browser Start contract] - -==== Browser API -https://github.com/elastic/kibana/blob/main/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionssetup.md[Browser Setup contract] -https://github.com/elastic/kibana/blob/main/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsstart.md[Browser Start contract] - From 853a0126fc38a7012d4307db9ef7bb56183cd17a Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 8 Nov 2021 21:06:07 +0100 Subject: [PATCH 10/98] migrations: handle 200 response code from _cluster/health API on timeout --- .../migrationsv2/actions/clone_index.ts | 4 ++-- .../actions/integration_tests/actions.test.ts | 17 +++++++++-------- .../actions/wait_for_index_status_yellow.ts | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/actions/clone_index.ts b/src/core/server/saved_objects/migrationsv2/actions/clone_index.ts index 5674535c80328..d7994f5a465d2 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/clone_index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/clone_index.ts @@ -127,11 +127,11 @@ export const cloneIndex = ({ // If the cluster state was updated and all shards ackd we're done return TaskEither.right(res); } else { - // Otherwise, wait until the target index has a 'green' status. + // Otherwise, wait until the target index has a 'yellow' status. return pipe( waitForIndexStatusYellow({ client, index: target, timeout }), TaskEither.map((value) => { - /** When the index status is 'green' we know that all shards were started */ + /** When the index status is 'yellow' we know that all shards were started */ return { acknowledged: true, shardsAcknowledged: true }; }) ); 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 173f2baefbb9b..b85fb0257d15c 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 @@ -410,14 +410,15 @@ describe('migration actions', () => { timeout: '0s', })(); - await expect(cloneIndexPromise).resolves.toMatchObject({ - _tag: 'Left', - left: { - error: expect.any(errors.ResponseError), - message: expect.stringMatching(/\"timed_out\":true/), - type: 'retryable_es_client_error', - }, - }); + await expect(cloneIndexPromise).resolves.toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "message": "Timeout waiting for the status of the [clone_red_index] index to become 'yellow'", + "type": "retryable_es_client_error", + }, + } + `); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts b/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts index 2880dfaff0d48..9d4df0ced8c0b 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts @@ -40,8 +40,18 @@ export const waitForIndexStatusYellow = }: WaitForIndexStatusYellowParams): TaskEither.TaskEither => () => { return client.cluster - .health({ index, wait_for_status: 'yellow', timeout }) - .then(() => { + .health({ + index, + wait_for_status: 'yellow', + timeout, + }) + .then((res) => { + if (res.body.timed_out === true) { + return Either.left({ + type: 'retryable_es_client_error' as const, + message: `Timeout waiting for the status of the [${index}] index to become 'yellow'`, + }); + } return Either.right({}); }) .catch(catchRetryableEsClientErrors); From 8f174b8063962176b0c0e99c7a8b664e3b2a879d Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Mon, 8 Nov 2021 15:09:45 -0500 Subject: [PATCH 11/98] Fixes issuse with dragging drop down. switch dropdown to use euiselect (#113960) * Fixes issuse with dragging drop down. switch dropdown to use euiselect * Update snapshots Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../dropdown_filter.stories.storyshot | 353 +++++++++++------- .../component/dropdown_filter.scss | 28 -- .../component/dropdown_filter.tsx | 25 +- .../filters/dropdown_filter/index.tsx | 5 +- .../workpad_interactive_page/index.js | 13 +- 5 files changed, 252 insertions(+), 172 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot index a14ad820586eb..52694d3b04089 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/__stories__/__snapshots__/dropdown_filter.stories.storyshot @@ -4,23 +4,42 @@ exports[`Storyshots renderers/DropdownFilter default 1`] = `
- - + +
+ + +
+
+
`; @@ -28,41 +47,60 @@ exports[`Storyshots renderers/DropdownFilter with choices 1`] = `
- - + +
+ + +
+
+ `; @@ -70,41 +108,60 @@ exports[`Storyshots renderers/DropdownFilter with choices and new value 1`] = `
- - + +
+ + +
+
+ `; @@ -112,41 +169,60 @@ exports[`Storyshots renderers/DropdownFilter with choices and value 1`] = `
- - + +
+ + +
+
+ `; @@ -154,22 +230,41 @@ exports[`Storyshots renderers/DropdownFilter with new value 1`] = `
- - + +
+ + +
+
+ `; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss index 425caee256d36..aa7c176f9d389 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.scss @@ -1,31 +1,3 @@ .canvasDropdownFilter { - width: 100%; - font-size: inherit; position: relative; - - .canvasDropdownFilter__select { - background-color: $euiColorEmptyShade; - width: 100%; - padding: $euiSizeXS $euiSize; - border: $euiBorderThin; - border-radius: $euiBorderRadius; - appearance: none; - font-size: inherit; - color: $euiTextColor; - - &:after { - display: none; - } - - &:focus { - box-shadow: none; - } - } - - .canvasDropdownFilter__icon { - position: absolute; - right: $euiSizeS; - top: $euiSizeS; - pointer-events: none; - } } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx index 5b020a2e194dc..ec9db940c00a1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx @@ -7,7 +7,7 @@ import React, { ChangeEvent, FocusEvent, FunctionComponent, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { EuiIcon } from '@elastic/eui'; +import { EuiSelect, EuiSelectOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; const strings = { @@ -57,30 +57,29 @@ export const DropdownFilter: FunctionComponent = ({ } }; - const dropdownOptions = options.map((option) => { + const dropdownOptions: EuiSelectOption[] = options.map((option) => { const { text } = option; const optionValue = option.value; const selected = optionValue === value; - return ( - - ); + return { + text, + selected, + value: optionValue, + }; }); - /* eslint-disable jsx-a11y/no-onchange */ return (
- - + options={dropdownOptions} + fullWidth + compressed + />
); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index d956c6291c609..001f2dc7652d2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -47,7 +47,10 @@ export const dropdownFilter: RendererFactory = () => ({ render(domNode, config, handlers) { let filterExpression = handlers.getFilter(); - if (filterExpression === undefined || !filterExpression.includes('exactly')) { + if ( + filterExpression !== '' && + (filterExpression === undefined || !filterExpression.includes('exactly')) + ) { filterExpression = ''; handlers.setFilter(filterExpression); } else if (filterExpression !== '') { diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js index f03547df8de99..25e64091f4aec 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js @@ -92,6 +92,16 @@ const isEmbeddableBody = (element) => { } }; +const isEuiSelect = (element) => { + const hasClosest = typeof element.closest === 'function'; + + if (hasClosest) { + return element.closest(`.euiSelect`); + } else { + return closest.call(element, `.euiSelect`); + } +}; + // Some elements in an embeddable may be portaled out of the embeddable container. // We do not want clicks on those to trigger drags, etc, in the workpad. This function // will check to make sure the clicked item is actually in the container @@ -243,7 +253,8 @@ export const InteractivePage = compose( })), withProps((...props) => ({ ...props, - canDragElement: (element) => !isEmbeddableBody(element) && isInWorkpad(element), + canDragElement: (element) => + !isEmbeddableBody(element) && !isEuiSelect(element) && isInWorkpad(element), })), withHandlers(eventHandlers), // Captures user intent, needs to have reconciled state () => InteractiveComponent From 6e18f3ff09fd0366d4797c4415f7d69fe5e1930f Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Mon, 8 Nov 2021 14:36:10 -0600 Subject: [PATCH 12/98] [ML] Fix data visualizer grid failing if one of the fields failed and not updating when refreshed (#115644) * [ML] Initial embed * [ML] Initial embed props * [ML] Add top nav link to data viz * Add visible fields * Add add data service to register links * Renames, refactor, use constants * Renames, refactor, use constants * Update tests and mocks * Embeddable * Update hook to update upon time udpate * Add filter support to query * Refactor filter utilities * Add filter support for embeddable * Fix saved search data undefined * Prototype aggregated view/document view switcher * Prototype flyout * Prototype save document view option in storage * Fix filter and query conflict with saved search * Minor styling edits * [ML] Initial embed * [ML] Initial embed props * Add embeddable 1 * Add visible fields * Embeddable 2 * Add filter support to query * Refactor filter utilities * Add filter support for embeddable * Fix saved search data undefined * Prototype aggregated view/document view switcher * Prototype flyout * Prototype save document view option in storage * Fix filter and query conflict with saved search * Minor styling edits * Fix missing code after conflicts * Remove dv locator and flyout * Make types happy * Fix types * Rename toggle option * Resolve conflicts * [ML] Reduce size of chart * [ML] Unbold name, switch icons of show distributions * [ML] Make size consistent * [ML] Make page size 25 * [ML] Switch to arrow right and down * [ML] Make legend font smaller * [ML] Add user setting * [ML] Add show preview by default setting * [ML] Match icon * Add panels around the subcontent * Add preference for aggregated vs doc * Fix types * Fix types, add constants for adv settings * Change to data view type * Temp fix for Kibana/EUI table overflow issue * Modify line height so text is not cut off, modify widths for varying screen sizes * Different width padders for different screens * Fix CI * Merge latest, move button to the right * Fix width for bar charts previews * Fix toggle buttons, fix maps * Delete unused file * Fix boolean styling * Change to enum, discover mode * Hide field stats * Hide field stats * Persist show mini preview/distribution settings * Remove window size, use size observer instead * Default to document view * Remove bold, switch icon * Set fixed width for top values, reduce font size in table * Fix custom url tests * Update width styling for panels * Fix missing flag for Discover sidebar, jest tests * Fix max width * Workaround for sorting * Fix import * Fix styling * Make height uniform, center alignment, fix map and keyword map not same size Move styling * Revert "Make height uniform, center alignment, fix map and keyword map not same size" This reverts commit 8fc42e2f * Revert "Make height uniform, center alignment, fix map and keyword map not same size" This reverts commit 8fc42e2f * Uniform height, left aligned, flex grid * Switch top values to have labels * Center content * Replace fixed widths with percentage * Fix table missing field types * Add dashboard embeddable and filter support * Fix file data viz styling and tests, lean up imports, remove hard coded pixels * Add search panel/kql filter bar * Temporarily fix scrolling * New kql filters for data visualizer * Set map height so it will fit the sampler shard size text * Use eui progress labels * Fix spacer * Add beta badge * Temporarily fix scrolling * Fix grow for Top Values for * [ML] Update functional tests to reflect new arrow icons * [ML] Add filter buttons and KQL bars * [ML] Update filter bar onChange behavior * [ML] Update top values filter onChange behavior * [ML] Update search filters when opening saved search * [ML] Clean up * [ML] Remove fit content for height * [ML] Fix boolean legend * [ML] Fix header section when browser width is small to large and when index pattern title is too large * [ML] Hide expander icon when dimension is xs or s & css fixes * [ML] Delete embeddables because they are not use * [ML] Rename view mode, refactor to separate hook, add error prompt if can't show, rename wrapper, clean up & fix tests * [ML] Make doc count 0 for empty fields, update t/f test * [ML] Add unit testing for search utils * Fix missing unsubscribe for embeddable output * Remove redundant onAddFilter for this PR, fix width * Rename Field Stats to Field stats to match convention * [ML] Fix expand all/collapse all behavior to override individual setting * [ML] Fix functional tests should be 0/0% * [ML] Fix docs content spacing, rename classnames, add filters to Discover, lens, and maps * [ML] Fix doc count for fields that exists but have no stats * [ML] Fix icon styling to match Discover but have text/keyword/histogram * [ML] Fix doc count for fields that exists but have no stats * [ML] Rename classnames to BEM style * Resolve latest changes * Add in place ss * Refactor helper functions * Refactor helper functions * Add error log * Migrate overall stats to data's search * Better handle errors * Fix url so restore session brings back correct view * Add progress bar * [ML] Add tests for data viz in Discover * [ML] Change to combinelatest * Update tests & dashboard behavior to reflect new advanced settings * Update telemetry * Remove workaround after eui bump fix * Remove dataloader * Snapshot * Migrate search to client side * Consolidate types * Change back to forkjoin instead of combinelatest for overallstats * Fix missing bool clause * Add login * Fix saved search attributes broken with latest changes * Update tests * Fix import * Match the no results found * Reset field stats so it reloads when query is refreshed * Reset field stats so it reloads when query is refreshed * Add doc stats * Merge to use hook completely * Merge to use hook completely * Fix doc chart doesn't show up when page is first mounted * Fix Discover auto refresh previously didn't update * Fix query util to return search source's results right away. Fix texts. * Refactor documentStats * Fix doc stats not showing upon page mount * Fix types * Delete old files * Update tests & i18n * Fix examples, tests * Remove old files & routes * Add telemetry, clean up, rename components for clarity * Fix size of callout message * Fix texts field * Consolidate field type * Consolidate field type, add count to top values * Clean up * Update tests * Remove progress on embedadble * Update snapshot * Clean up, consolidate searchOptions * Fix new es client types * Fix types * Fix loading state in Discover * Remove unused services, Change switchMap to map, mergeMap -> switchMap, update types * Fix missing filters * Fix message of table to show searching instead of no items found * Fix dashboard saved search source persisting time range * [ML] Fix table message state * [ML] Fix to not fetch field stats if cardinality is 0 * [ML] Fix locator missing view mode * [ML] Quit right away if field doesn't exist in docs * [ML] Change to use batch and only retry with individual field if failed * [ML] Batch requests for speed and retry failures for resiliency * No need to fetch field stats if overall stats haven't completed * Wait on overallStats to complete * Fix types after merge * Fix payload size too big 413, num of requests * Update field icon to using kbn/react-field package Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/field_icon.test.tsx.snap | 30 + .../src/field_icon/field_icon.tsx | 3 + .../components/field_stats_table/constants.ts | 12 + .../field_stats_table/field_stats_table.tsx} | 57 +- ...d_stats_table_saved_search_embeddable.tsx} | 9 +- .../components/field_stats_table}/index.ts | 3 +- .../components/layout/discover_layout.tsx | 22 +- .../components/sidebar/discover_field.tsx | 8 +- .../sidebar/discover_sidebar.test.tsx | 2 +- .../sidebar/lib/get_field_type_name.ts | 9 + .../main/services/discover_state.ts | 2 + .../embeddable/saved_search_embeddable.tsx | 6 +- src/plugins/discover/public/locator.ts | 15 + src/plugins/discover/public/url_generator.ts | 9 + .../services/time_buckets.d.ts | 2 +- .../services/time_buckets.js | 6 +- .../common/types/field_request_config.ts | 10 +- .../types/field_stats.ts} | 128 +++- .../types/field_vis_config.ts | 5 +- .../data_visualizer/common/types/index.ts | 1 - .../util => common/utils}/parse_interval.ts | 0 .../common/utils/query_utils.ts | 8 +- .../document_count_content.tsx | 23 +- .../expanded_row/file_based_expanded_row.tsx | 2 +- .../expanded_row/index_based_expanded_row.tsx | 5 + .../field_names_filter/field_names_filter.tsx | 2 +- .../field_type_icon.test.tsx.snap | 8 +- .../field_type_icon/field_type_icon.test.tsx | 4 +- .../field_type_icon/field_type_icon.tsx | 97 +-- .../field_types_filter/field_types_filter.tsx | 4 +- .../fields_stats_grid/fields_stats_grid.tsx | 2 +- .../fields_stats_grid/filter_fields.ts | 2 +- .../field_data_expanded_row/error_message.tsx | 29 + .../field_data_expanded_row/ip_content.tsx | 14 +- .../field_data_expanded_row/text_content.tsx | 3 +- .../field_data_row/use_column_chart.test.tsx | 4 +- .../field_data_row/use_column_chart.tsx | 4 +- .../data_visualizer_stats_table.tsx | 14 +- .../stats_table/types/field_data_row.ts | 5 +- .../components/stats_table/types/index.ts | 8 +- .../components/top_values/top_values.tsx | 10 +- .../common/util/field_types_utils.test.ts | 15 +- .../common/util/field_types_utils.ts | 46 +- .../common/util/parse_interval.test.ts | 2 +- .../full_time_range_selector.tsx | 1 - .../index_data_visualizer_view.tsx | 592 ++---------------- .../search_panel/field_type_filter.tsx | 2 +- .../components/search_panel/search_panel.tsx | 3 +- .../data_loader/data_loader.ts | 142 ----- .../grid_embeddable/grid_embeddable.tsx | 27 +- .../use_data_visualizer_grid_data.ts | 531 +++++++--------- .../hooks/use_field_stats.ts | 287 +++++++++ .../hooks/use_overall_stats.ts | 267 ++++++++ .../index_data_visualizer/progress_utils.ts | 21 + .../search_strategy/requests}/constants.ts | 4 + .../requests/get_boolean_field_stats.ts | 110 ++++ .../requests/get_date_field_stats.ts | 106 ++++ .../requests/get_document_stats.ts | 89 +++ .../requests/get_field_examples.ts | 114 ++++ .../requests/get_fields_stats.ts | 50 ++ .../requests/get_numeric_field_stats.ts | 207 ++++++ .../requests/get_string_field_stats.ts | 139 ++++ .../search_strategy/requests/overall_stats.ts | 227 +++++++ .../services/visualizer_stats.ts | 98 --- .../types/combined_query.ts | 4 +- .../types/overall_stats.ts | 7 +- .../utils/error_utils.ts | 2 +- .../utils}/process_distribution_data.ts | 4 +- .../utils/saved_search_utils.test.ts | 2 +- .../utils/saved_search_utils.ts | 23 +- .../data_visualizer/check_fields_exist.ts | 183 ------ .../models/data_visualizer/data_visualizer.ts | 489 --------------- .../data_visualizer/get_field_examples.ts | 80 --- .../data_visualizer/get_fields_stats.ts | 478 -------------- .../get_histogram_for_fields.ts | 186 ------ .../server/models/data_visualizer/index.ts | 8 - .../plugins/data_visualizer/server/plugin.ts | 3 - .../data_visualizer/server/routes/index.ts | 8 - .../data_visualizer/server/routes/routes.ts | 262 -------- .../server/routes/schemas/index.ts | 8 - .../schemas/index_data_visualizer_schemas.ts | 76 --- .../data_visualizer/server/types/deps.ts | 6 + .../data_visualizer/server/types/index.ts | 1 - .../translations/translations/ja-JP.json | 9 - .../translations/translations/zh-CN.json | 9 - .../data_visualizer/get_field_histograms.ts | 121 ---- .../ml/data_visualizer/get_field_stats.ts | 234 ------- .../ml/data_visualizer/get_overall_stats.ts | 153 ----- .../apis/ml/data_visualizer/index.ts | 15 - x-pack/test/api_integration/apis/ml/index.ts | 1 - .../test/api_integration_basic/apis/index.ts | 1 - .../apis/ml/data_visualizer/index.ts | 15 - .../api_integration_basic/apis/ml/index.ts | 35 -- ...ata_visualizer_index_pattern_management.ts | 18 +- .../apps/ml/data_visualizer/types.ts | 6 +- 95 files changed, 2373 insertions(+), 3741 deletions(-) create mode 100644 src/plugins/discover/public/application/components/field_stats_table/constants.ts rename src/plugins/discover/public/{components/data_visualizer_grid/data_visualizer_grid.tsx => application/components/field_stats_table/field_stats_table.tsx} (78%) rename src/plugins/discover/public/{components/data_visualizer_grid/field_stats_table_embeddable.tsx => application/components/field_stats_table/field_stats_table_saved_search_embeddable.tsx} (78%) rename src/plugins/discover/public/{components/data_visualizer_grid => application/components/field_stats_table}/index.ts (68%) rename x-pack/plugins/data_visualizer/{public/application/index_data_visualizer => common}/services/time_buckets.d.ts (96%) rename x-pack/plugins/data_visualizer/{public/application/index_data_visualizer => common}/services/time_buckets.js (98%) rename x-pack/plugins/data_visualizer/{server/types/chart_data.ts => common/types/field_stats.ts} (50%) rename x-pack/plugins/data_visualizer/{public/application/common/components/stats_table => common}/types/field_vis_config.ts (92%) rename x-pack/plugins/data_visualizer/{public/application/common/util => common/utils}/parse_interval.ts (100%) create mode 100644 x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx delete mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts rename x-pack/plugins/data_visualizer/public/application/index_data_visualizer/{embeddables/grid_embeddable => hooks}/use_data_visualizer_grid_data.ts (52%) create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/progress_utils.ts rename x-pack/plugins/data_visualizer/{server/models/data_visualizer => public/application/index_data_visualizer/search_strategy/requests}/constants.ts (81%) create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts create mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts delete mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/visualizer_stats.ts rename x-pack/plugins/data_visualizer/{server/models/data_visualizer => public/application/index_data_visualizer/utils}/process_distribution_data.ts (95%) delete mode 100644 x-pack/plugins/data_visualizer/server/models/data_visualizer/check_fields_exist.ts delete mode 100644 x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts delete mode 100644 x-pack/plugins/data_visualizer/server/models/data_visualizer/get_field_examples.ts delete mode 100644 x-pack/plugins/data_visualizer/server/models/data_visualizer/get_fields_stats.ts delete mode 100644 x-pack/plugins/data_visualizer/server/models/data_visualizer/get_histogram_for_fields.ts delete mode 100644 x-pack/plugins/data_visualizer/server/models/data_visualizer/index.ts delete mode 100644 x-pack/plugins/data_visualizer/server/routes/index.ts delete mode 100644 x-pack/plugins/data_visualizer/server/routes/routes.ts delete mode 100644 x-pack/plugins/data_visualizer/server/routes/schemas/index.ts delete mode 100644 x-pack/plugins/data_visualizer/server/routes/schemas/index_data_visualizer_schemas.ts delete mode 100644 x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts delete mode 100644 x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts delete mode 100644 x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts delete mode 100644 x-pack/test/api_integration/apis/ml/data_visualizer/index.ts delete mode 100644 x-pack/test/api_integration_basic/apis/ml/data_visualizer/index.ts delete mode 100644 x-pack/test/api_integration_basic/apis/ml/index.ts diff --git a/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap index f6870a5209c1e..0e9ae4ee2aaaa 100644 --- a/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap +++ b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap @@ -95,6 +95,16 @@ exports[`FieldIcon renders known field types geo_shape is rendered 1`] = ` /> `; +exports[`FieldIcon renders known field types histogram is rendered 1`] = ` + +`; + exports[`FieldIcon renders known field types ip is rendered 1`] = ` `; +exports[`FieldIcon renders known field types keyword is rendered 1`] = ` + +`; + exports[`FieldIcon renders known field types murmur3 is rendered 1`] = ` `; +exports[`FieldIcon renders known field types text is rendered 1`] = ` + +`; + exports[`FieldIcon renders with className if provided 1`] = ` > = { murmur3: { iconType: 'tokenFile' }, number: { iconType: 'tokenNumber' }, number_range: { iconType: 'tokenNumber' }, + histogram: { iconType: 'tokenHistogram' }, _source: { iconType: 'editorCodeBlock', color: 'gray' }, string: { iconType: 'tokenString' }, + text: { iconType: 'tokenString' }, + keyword: { iconType: 'tokenKeyword' }, nested: { iconType: 'tokenNested' }, }; diff --git a/src/plugins/discover/public/application/components/field_stats_table/constants.ts b/src/plugins/discover/public/application/components/field_stats_table/constants.ts new file mode 100644 index 0000000000000..bf1a36da59ecf --- /dev/null +++ b/src/plugins/discover/public/application/components/field_stats_table/constants.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. + */ + +/** Telemetry related to field statistics table **/ +export const FIELD_STATISTICS_LOADED = 'field_statistics_loaded'; +export const FIELD_STATISTICS_VIEW_CLICK = 'field_statistics_view_click'; +export const DOCUMENTS_VIEW_CLICK = 'documents_view_click'; diff --git a/src/plugins/discover/public/components/data_visualizer_grid/data_visualizer_grid.tsx b/src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx similarity index 78% rename from src/plugins/discover/public/components/data_visualizer_grid/data_visualizer_grid.tsx rename to src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx index 511aa90f5f4a4..5061ab0ba3746 100644 --- a/src/plugins/discover/public/components/data_visualizer_grid/data_visualizer_grid.tsx +++ b/src/plugins/discover/public/application/components/field_stats_table/field_stats_table.tsx @@ -7,18 +7,21 @@ */ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { Filter } from '@kbn/es-query'; -import { IndexPatternField, IndexPattern, DataView, Query } from '../../../../data/common'; -import { DiscoverServices } from '../../build_services'; +import type { Filter } from '@kbn/es-query'; +import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; +import { IndexPatternField, IndexPattern, DataView, Query } from '../../../../../data/common'; +import type { DiscoverServices } from '../../../build_services'; import { EmbeddableInput, EmbeddableOutput, ErrorEmbeddable, IEmbeddable, isErrorEmbeddable, -} from '../../../../embeddable/public'; -import { SavedSearch } from '../../services/saved_searches'; -import { GetStateReturn } from '../../application/main/services/discover_state'; +} from '../../../../../embeddable/public'; +import { FIELD_STATISTICS_LOADED } from './constants'; +import type { SavedSearch } from '../../../services/saved_searches'; +import type { GetStateReturn } from '../../main/services/discover_state'; +import { DataRefetch$ } from '../../main/utils/use_saved_search'; export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { indexPattern: IndexPattern; @@ -36,7 +39,7 @@ export interface DataVisualizerGridEmbeddableOutput extends EmbeddableOutput { showDistributions?: boolean; } -export interface DiscoverDataVisualizerGridProps { +export interface FieldStatisticsTableProps { /** * Determines which columns are displayed */ @@ -69,14 +72,24 @@ export interface DiscoverDataVisualizerGridProps { * Filters query to update the table content */ filters?: Filter[]; + /** + * State container with persisted settings + */ stateContainer?: GetStateReturn; /** * Callback to add a filter to filter bar */ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + /** + * Metric tracking function + * @param metricType + * @param eventName + */ + trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; + savedSearchRefetch$?: DataRefetch$; } -export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProps) => { +export const FieldStatisticsTable = (props: FieldStatisticsTableProps) => { const { services, indexPattern, @@ -86,9 +99,10 @@ export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProp filters, stateContainer, onAddFilter, + trackUiMetric, + savedSearchRefetch$, } = props; const { uiSettings } = services; - const [embeddable, setEmbeddable] = useState< | ErrorEmbeddable | IEmbeddable @@ -109,10 +123,16 @@ export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProp } }); + const refetch = savedSearchRefetch$?.subscribe(() => { + if (embeddable && !isErrorEmbeddable(embeddable)) { + embeddable.updateInput({ lastReloadRequestTime: Date.now() }); + } + }); return () => { sub?.unsubscribe(); + refetch?.unsubscribe(); }; - }, [embeddable, stateContainer]); + }, [embeddable, stateContainer, savedSearchRefetch$]); useEffect(() => { if (embeddable && !isErrorEmbeddable(embeddable)) { @@ -135,17 +155,11 @@ export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProp embeddable.updateInput({ showPreviewByDefault, }); + embeddable.reload(); } }, [showPreviewByDefault, uiSettings, embeddable]); - useEffect(() => { - return () => { - // Clean up embeddable upon unmounting - embeddable?.destroy(); - }; - }, [embeddable]); - useEffect(() => { let unmounted = false; const loadEmbeddable = async () => { @@ -181,8 +195,15 @@ export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProp useEffect(() => { if (embeddableRoot.current && embeddable) { embeddable.render(embeddableRoot.current); + + trackUiMetric?.(METRIC_TYPE.LOADED, FIELD_STATISTICS_LOADED); } - }, [embeddable, embeddableRoot, uiSettings]); + + return () => { + // Clean up embeddable upon unmounting + embeddable?.destroy(); + }; + }, [embeddable, embeddableRoot, uiSettings, trackUiMetric]); return (
- { stateContainer.setAppState({ viewMode: mode }); + + if (trackUiMetric) { + if (mode === VIEW_MODE.AGGREGATED_LEVEL) { + trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK); + } else { + trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK); + } + } }, - [stateContainer] + [trackUiMetric, stateContainer] ); const fetchCounter = useRef(0); @@ -315,7 +327,7 @@ export function DiscoverLayout({ stateContainer={stateContainer} /> ) : ( - )} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 678eddcdf02c0..6864a1c5c2d4a 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -59,9 +59,11 @@ const FieldInfoIcon: React.FC = memo(() => ( )); -const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => ( - -)); +const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => { + // If it's a string type, we want to distinguish between keyword and text + const tempType = field.type === 'string' && field.esTypes ? field.esTypes[0] : field.type; + return ; +}); const FieldName: React.FC<{ field: IndexPatternField }> = memo(({ field }) => { const title = diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx index b2b5c8a056995..9dd7ef19ffc07 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { each, cloneDeep } from 'lodash'; +import { cloneDeep, each } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-expect-error diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts index e2d4c2f7ddcf2..f68395593bd8b 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_name.ts @@ -51,6 +51,15 @@ export function getFieldTypeName(type: string) { return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { defaultMessage: 'String field', }); + case 'text': + return i18n.translate('discover.fieldNameIcons.textFieldAriaLabel', { + defaultMessage: 'Text field', + }); + case 'keyword': + return i18n.translate('discover.fieldNameIcons.keywordFieldAriaLabel', { + defaultMessage: 'Keyword field', + }); + case 'nested': return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { defaultMessage: 'Nested field', diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 66e013e8f20ea..0b855a27cc74e 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -411,5 +411,7 @@ function createUrlGeneratorState({ } : undefined, useHash: false, + viewMode: appState.viewMode, + hideAggregatedPreview: appState.hideAggregatedPreview, }; } diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 914b9f25d29ae..c04e6515cfbe1 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -45,9 +45,9 @@ import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DocTableProps } from '../components/doc_table/doc_table_wrapper'; import { getDefaultSort } from '../components/doc_table'; import { SortOrder } from '../components/doc_table/components/table_header/helpers'; -import { updateSearchSource } from './utils/update_search_source'; import { VIEW_MODE } from '../components/view_mode_toggle'; -import { FieldStatsTableEmbeddable } from '../components/data_visualizer_grid/field_stats_table_embeddable'; +import { updateSearchSource } from './utils/update_search_source'; +import { FieldStatsTableSavedSearchEmbeddable } from '../application/components/field_stats_table'; export type SearchProps = Partial & Partial & { @@ -391,7 +391,7 @@ export class SavedSearchEmbeddable Array.isArray(searchProps.columns) ) { ReactDOM.render( - ; @@ -102,6 +111,8 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition esFilters.isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; + if (viewMode) appState.viewMode = viewMode; + if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview; let path = `#/${savedSearchPath}`; path = setStateToKbnUrl('_g', queryState, { useHash }, path); diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index 7cc729fd7f7e5..32e89691574df 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -10,6 +10,7 @@ import type { UrlGeneratorsDefinition } from '../../share/public'; import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; +import { VIEW_MODE } from './components/view_mode_toggle'; export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR'; @@ -75,6 +76,8 @@ export interface DiscoverUrlGeneratorState { * id of the used saved query */ savedQuery?: string; + viewMode?: VIEW_MODE; + hideAggregatedPreview?: boolean; } interface Params { @@ -104,6 +107,8 @@ export class DiscoverUrlGenerator savedQuery, sort, interval, + viewMode, + hideAggregatedPreview, }: DiscoverUrlGeneratorState): Promise => { const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : ''; const appState: { @@ -114,6 +119,8 @@ export class DiscoverUrlGenerator interval?: string; sort?: string[][]; savedQuery?: string; + viewMode?: VIEW_MODE; + hideAggregatedPreview?: boolean; } = {}; const queryState: QueryState = {}; @@ -130,6 +137,8 @@ export class DiscoverUrlGenerator if (filters && filters.length) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; + if (viewMode) appState.viewMode = viewMode; + if (hideAggregatedPreview) appState.hideAggregatedPreview = hideAggregatedPreview; let url = `${this.params.appBasePath}#/${savedSearchPath}`; url = setStateToKbnUrl('_g', queryState, { useHash }, url); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts b/x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts similarity index 96% rename from x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts rename to x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts index 9a5410918a099..62a3187be47dc 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.d.ts +++ b/x-pack/plugins/data_visualizer/common/services/time_buckets.d.ts @@ -31,7 +31,7 @@ export declare class TimeBuckets { public setMaxBars(maxBars: number): void; public setInterval(interval: string): void; public setBounds(bounds: TimeRangeBounds): void; - public getBounds(): { min: any; max: any }; + public getBounds(): { min: Moment; max: Moment }; public getInterval(): TimeBucketsInterval; public getScaledDateFormat(): string; } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js b/x-pack/plugins/data_visualizer/common/services/time_buckets.js similarity index 98% rename from x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js rename to x-pack/plugins/data_visualizer/common/services/time_buckets.js index 5d54b6c936fb2..49de535ee6c26 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_buckets.js +++ b/x-pack/plugins/data_visualizer/common/services/time_buckets.js @@ -5,12 +5,12 @@ * 2.0. */ -import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; -import { UI_SETTINGS } from '../../../../../../../src/plugins/data/common'; +import { FIELD_FORMAT_IDS } from '../../../../../src/plugins/field_formats/common'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { ary, assign, isPlainObject, isString, sortBy } from 'lodash'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { parseInterval } from '../../common/util/parse_interval'; +import { parseInterval } from '../utils/parse_interval'; const { duration: d } = moment; diff --git a/x-pack/plugins/data_visualizer/common/types/field_request_config.ts b/x-pack/plugins/data_visualizer/common/types/field_request_config.ts index 36e8fe14b7002..f0ea7079bf750 100644 --- a/x-pack/plugins/data_visualizer/common/types/field_request_config.ts +++ b/x-pack/plugins/data_visualizer/common/types/field_request_config.ts @@ -14,7 +14,7 @@ export interface Percentile { } export interface FieldRequestConfig { - fieldName?: string; + fieldName: string; type: JobFieldType; cardinality: number; } @@ -29,6 +29,7 @@ export interface DocumentCounts { } export interface FieldVisStats { + error?: Error; cardinality?: number; count?: number; sampleCount?: number; @@ -58,3 +59,10 @@ export interface FieldVisStats { timeRangeEarliest?: number; timeRangeLatest?: number; } + +export interface DVErrorObject { + causedBy?: string; + message: string; + statusCode?: number; + fullError?: Error; +} diff --git a/x-pack/plugins/data_visualizer/server/types/chart_data.ts b/x-pack/plugins/data_visualizer/common/types/field_stats.ts similarity index 50% rename from x-pack/plugins/data_visualizer/server/types/chart_data.ts rename to x-pack/plugins/data_visualizer/common/types/field_stats.ts index 99c23cf88b5ba..8932a0641cbe6 100644 --- a/x-pack/plugins/data_visualizer/server/types/chart_data.ts +++ b/x-pack/plugins/data_visualizer/common/types/field_stats.ts @@ -5,6 +5,12 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Query } from '@kbn/es-query'; +import { isPopulatedObject } from '../utils/object_utils'; +import { IKibanaSearchResponse } from '../../../../../src/plugins/data/common'; +import { TimeBucketsInterval } from '../services/time_buckets'; + export interface FieldData { fieldName: string; existsInDocs: boolean; @@ -19,6 +25,12 @@ export interface Field { fieldName: string; type: string; cardinality: number; + safeFieldName: string; +} + +// @todo: check +export function isValidField(arg: unknown): arg is Field { + return isPopulatedObject(arg, ['fieldName', 'type']) && typeof arg.fieldName === 'string'; } export interface HistogramField { @@ -27,19 +39,25 @@ export interface HistogramField { } export interface Distribution { - percentiles: any[]; + percentiles: Array<{ value?: number; percent: number; minValue: number; maxValue: number }>; minPercentile: number; maxPercentile: number; } -export interface Aggs { - [key: string]: any; -} - export interface Bucket { doc_count: number; } +export interface FieldStatsError { + fieldName?: string; + fields?: Field[]; + error: Error; +} + +export const isIKibanaSearchResponse = (arg: unknown): arg is IKibanaSearchResponse => { + return isPopulatedObject(arg, ['rawResponse']); +}; + export interface NumericFieldStats { fieldName: string; count: number; @@ -78,15 +96,15 @@ export interface BooleanFieldStats { } export interface DocumentCountStats { - documentCounts: { - interval: number; - buckets: { [key: string]: number }; - }; + interval: number; + buckets: { [key: string]: number }; + timeRangeEarliest: number; + timeRangeLatest: number; } export interface FieldExamples { fieldName: string; - examples: any[]; + examples: unknown[]; } export interface NumericColumnStats { @@ -97,10 +115,7 @@ export interface NumericColumnStats { export type NumericColumnStatsMap = Record; export interface AggHistogram { - histogram: { - field: string; - interval: number; - }; + histogram: estypes.AggregationsHistogramAggregation; } export interface AggTerms { @@ -142,17 +157,8 @@ export interface UnsupportedChartData { type: 'unsupported'; } -export interface FieldAggCardinality { - field: string; - percent?: any; -} - -export interface ScriptAggCardinality { - script: any; -} - export interface AggCardinality { - cardinality: FieldAggCardinality | ScriptAggCardinality; + cardinality: estypes.AggregationsCardinalityAggregation; } export type ChartRequestAgg = AggHistogram | AggCardinality | AggTerms; @@ -166,3 +172,77 @@ export type BatchStats = | DateFieldStats | DocumentCountStats | FieldExamples; + +export type FieldStats = + | NumericFieldStats + | StringFieldStats + | BooleanFieldStats + | DateFieldStats + | FieldExamples + | FieldStatsError; + +export function isValidFieldStats(arg: unknown): arg is FieldStats { + return isPopulatedObject(arg, ['fieldName', 'type', 'count']); +} + +export interface FieldStatsCommonRequestParams { + index: string; + samplerShardSize: number; + timeFieldName?: string; + earliestMs?: number | undefined; + latestMs?: number | undefined; + runtimeFieldMap?: estypes.MappingRuntimeFields; + intervalMs?: number; + query: estypes.QueryDslQueryContainer; + maxExamples?: number; +} + +export interface OverallStatsSearchStrategyParams { + sessionId?: string; + earliest?: number; + latest?: number; + aggInterval: TimeBucketsInterval; + intervalMs?: number; + searchQuery: Query['query']; + samplerShardSize: number; + index: string; + timeFieldName?: string; + runtimeFieldMap?: estypes.MappingRuntimeFields; + aggregatableFields: string[]; + nonAggregatableFields: string[]; +} + +export interface FieldStatsSearchStrategyReturnBase { + progress: DataStatsFetchProgress; + fieldStats: Map | undefined; + startFetch: () => void; + cancelFetch: () => void; +} + +export interface DataStatsFetchProgress { + error?: Error; + isRunning: boolean; + loaded: number; + total: number; +} + +export interface FieldData { + fieldName: string; + existsInDocs: boolean; + stats?: { + sampleCount?: number; + count?: number; + cardinality?: number; + }; +} + +export interface Field { + fieldName: string; + type: string; + cardinality: number; + safeFieldName: string; +} + +export interface Aggs { + [key: string]: estypes.AggregationsAggregationContainer; +} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts b/x-pack/plugins/data_visualizer/common/types/field_vis_config.ts similarity index 92% rename from x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts rename to x-pack/plugins/data_visualizer/common/types/field_vis_config.ts index eeb9fe12692fd..dcd7da74b85ef 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts +++ b/x-pack/plugins/data_visualizer/common/types/field_vis_config.ts @@ -5,8 +5,7 @@ * 2.0. */ -import type { Percentile, JobFieldType, FieldVisStats } from '../../../../../../common/types'; - +import type { Percentile, JobFieldType, FieldVisStats } from './index'; export interface MetricFieldVisStats { avg?: number; distribution?: { @@ -23,7 +22,7 @@ export interface MetricFieldVisStats { // which display the field information. export interface FieldVisConfig { type: JobFieldType; - fieldName?: string; + fieldName: string; displayName?: string; existsInDocs: boolean; aggregatable: boolean; diff --git a/x-pack/plugins/data_visualizer/common/types/index.ts b/x-pack/plugins/data_visualizer/common/types/index.ts index 1153b45e1cce2..381f7a556b18d 100644 --- a/x-pack/plugins/data_visualizer/common/types/index.ts +++ b/x-pack/plugins/data_visualizer/common/types/index.ts @@ -15,7 +15,6 @@ export type { FieldVisStats, Percentile, } from './field_request_config'; -export type InputData = any[]; export interface DataVisualizerTableState { pageSize: number; diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.ts b/x-pack/plugins/data_visualizer/common/utils/parse_interval.ts similarity index 100% rename from x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.ts rename to x-pack/plugins/data_visualizer/common/utils/parse_interval.ts diff --git a/x-pack/plugins/data_visualizer/common/utils/query_utils.ts b/x-pack/plugins/data_visualizer/common/utils/query_utils.ts index 2aa4cd063d1b1..dc21bbcae96c3 100644 --- a/x-pack/plugins/data_visualizer/common/utils/query_utils.ts +++ b/x-pack/plugins/data_visualizer/common/utils/query_utils.ts @@ -6,6 +6,8 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Query } from '@kbn/es-query'; + /* * Contains utility functions for building and processing queries. */ @@ -16,8 +18,8 @@ export function buildBaseFilterCriteria( timeFieldName?: string, earliestMs?: number, latestMs?: number, - query?: object -) { + query?: Query['query'] +): estypes.QueryDslQueryContainer[] { const filterCriteria = []; if (timeFieldName && earliestMs && latestMs) { filterCriteria.push({ @@ -31,7 +33,7 @@ export function buildBaseFilterCriteria( }); } - if (query) { + if (query && typeof query === 'object') { filterCriteria.push(query); } diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx index d49dbdc7cb446..832e18a12369f 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_content.tsx @@ -7,30 +7,25 @@ import React, { FC } from 'react'; import { DocumentCountChart, DocumentCountChartPoint } from './document_count_chart'; -import { FieldVisConfig, FileBasedFieldVisConfig } from '../stats_table/types'; import { TotalCountHeader } from './total_count_header'; +import { DocumentCountStats } from '../../../../../common/types/field_stats'; export interface Props { - config?: FieldVisConfig | FileBasedFieldVisConfig; + documentCountStats?: DocumentCountStats; totalCount: number; } -export const DocumentCountContent: FC = ({ config, totalCount }) => { - if (config?.stats === undefined) { +export const DocumentCountContent: FC = ({ documentCountStats, totalCount }) => { + if (documentCountStats === undefined) { return totalCount !== undefined ? : null; } - const { documentCounts, timeRangeEarliest, timeRangeLatest } = config.stats; - if ( - documentCounts === undefined || - timeRangeEarliest === undefined || - timeRangeLatest === undefined - ) - return null; + const { timeRangeEarliest, timeRangeLatest } = documentCountStats; + if (timeRangeEarliest === undefined || timeRangeLatest === undefined) return null; let chartPoints: DocumentCountChartPoint[] = []; - if (documentCounts.buckets !== undefined) { - const buckets: Record = documentCounts?.buckets; + if (documentCountStats.buckets !== undefined) { + const buckets: Record = documentCountStats?.buckets; chartPoints = Object.entries(buckets).map(([time, value]) => ({ time: +time, value })); } @@ -41,7 +36,7 @@ export const DocumentCountContent: FC = ({ config, totalCount }) => { chartPoints={chartPoints} timeRangeEarliest={timeRangeEarliest} timeRangeLatest={timeRangeLatest} - interval={documentCounts.interval} + interval={documentCountStats.interval} /> ); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx index 8a9f9a25c16fa..7ba1615e22b43 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/file_based_expanded_row.tsx @@ -17,7 +17,7 @@ import { } from '../stats_table/components/field_data_expanded_row'; import { GeoPointContent } from './geo_point_content/geo_point_content'; import { JOB_FIELD_TYPES } from '../../../../../common'; -import type { FileBasedFieldVisConfig } from '../stats_table/types/field_vis_config'; +import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config'; export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFieldVisConfig }) => { const config = item; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx index 79af35f1c8005..b87da2b3da789 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx @@ -23,6 +23,7 @@ import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { CombinedQuery } from '../../../index_data_visualizer/types/combined_query'; import { LoadingIndicator } from '../loading_indicator'; import { IndexPatternField } from '../../../../../../../../src/plugins/data/common'; +import { ErrorMessageContent } from '../stats_table/components/field_data_expanded_row/error_message'; export const IndexBasedDataVisualizerExpandedRow = ({ item, @@ -46,6 +47,10 @@ export const IndexBasedDataVisualizerExpandedRow = ({ return ; } + if (config.stats?.error) { + return ; + } + switch (type) { case JOB_FIELD_TYPES.NUMBER: return ; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx index 88b4cd406b33c..58e9b9b5740dc 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_names_filter/field_names_filter.tsx @@ -11,7 +11,7 @@ import { MultiSelectPicker } from '../multi_select_picker'; import type { FileBasedFieldVisConfig, FileBasedUnknownFieldVisConfig, -} from '../stats_table/types/field_vis_config'; +} from '../../../../../common/types/field_vis_config'; interface Props { fields: Array; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap index 398dc5dad2dc7..af4464cbc6b4e 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap @@ -3,15 +3,13 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = ` - `; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx index b6a5ff3e5dbed..0c036dd6c6d76 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx @@ -14,7 +14,7 @@ import { JOB_FIELD_TYPES } from '../../../../../common'; describe('FieldTypeIcon', () => { test(`render component when type matches a field type`, () => { const typeIconComponent = shallow( - + ); expect(typeIconComponent).toMatchSnapshot(); }); @@ -24,7 +24,7 @@ describe('FieldTypeIcon', () => { jest.useFakeTimers(); const typeIconComponent = mount( - + ); expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx index 9d803e3d4a80c..2a9767ccd62b1 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx @@ -6,103 +6,32 @@ */ import React, { FC } from 'react'; -import { EuiToken, EuiToolTip } from '@elastic/eui'; +import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getJobTypeAriaLabel } from '../../util/field_types_utils'; +import { FieldIcon } from '@kbn/react-field/field_icon'; +import { getJobTypeLabel } from '../../util/field_types_utils'; import type { JobFieldType } from '../../../../../common'; import './_index.scss'; interface FieldTypeIconProps { tooltipEnabled: boolean; type: JobFieldType; - needsAria: boolean; } -interface FieldTypeIconContainerProps { - ariaLabel: string | null; - iconType: string; - color?: string; - needsAria: boolean; - [key: string]: any; -} - -// defaultIcon => a unknown datatype -const defaultIcon = { iconType: 'questionInCircle', color: 'gray' }; - -// Extended & modified version of src/plugins/kibana_react/public/field_icon/field_icon.tsx -export const typeToEuiIconMap: Record = { - boolean: { iconType: 'tokenBoolean' }, - // icon for a data view mapping conflict in discover - conflict: { iconType: 'alert', color: 'euiColorVis9' }, - date: { iconType: 'tokenDate' }, - date_range: { iconType: 'tokenDate' }, - geo_point: { iconType: 'tokenGeo' }, - geo_shape: { iconType: 'tokenGeo' }, - ip: { iconType: 'tokenIP' }, - ip_range: { iconType: 'tokenIP' }, - // is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html - murmur3: { iconType: 'tokenFile' }, - number: { iconType: 'tokenNumber' }, - number_range: { iconType: 'tokenNumber' }, - histogram: { iconType: 'tokenHistogram' }, - _source: { iconType: 'editorCodeBlock', color: 'gray' }, - string: { iconType: 'tokenString' }, - text: { iconType: 'tokenString' }, - keyword: { iconType: 'tokenString' }, - nested: { iconType: 'tokenNested' }, -}; - -export const FieldTypeIcon: FC = ({ - tooltipEnabled = false, - type, - needsAria = true, -}) => { - const ariaLabel = getJobTypeAriaLabel(type); - const token = typeToEuiIconMap[type] || defaultIcon; - const containerProps = { ...token, ariaLabel, needsAria }; - +export const FieldTypeIcon: FC = ({ tooltipEnabled = false, type }) => { + const label = + getJobTypeLabel(type) ?? + i18n.translate('xpack.dataVisualizer.fieldTypeIcon.fieldTypeTooltip', { + defaultMessage: '{type} type', + values: { type }, + }); if (tooltipEnabled === true) { return ( - - + + ); } - return ; -}; - -// If the tooltip is used, it will apply its events to its first inner child. -// To pass on its properties we apply `rest` to the outer `span` element. -const FieldTypeIconContainer: FC = ({ - ariaLabel, - iconType, - color, - needsAria, - ...rest -}) => { - const wrapperProps: { className: string; 'aria-label'?: string } = { - className: 'field-type-icon', - }; - if (needsAria && ariaLabel) { - wrapperProps['aria-label'] = ariaLabel; - } - return ( - - ); + return ; }; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx index 97dc2077d5931..0fa860bc6f55e 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx @@ -12,7 +12,7 @@ import { MultiSelectPicker, Option } from '../multi_select_picker'; import type { FileBasedFieldVisConfig, FileBasedUnknownFieldVisConfig, -} from '../stats_table/types/field_vis_config'; +} from '../../../../../common/types/field_vis_config'; import { FieldTypeIcon } from '../field_type_icon'; import { jobTypeLabels } from '../../util/field_types_utils'; @@ -50,7 +50,7 @@ export const DataVisualizerFieldTypesFilter: FC = ({ {label} {type && ( - + )} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx index b57072eed2944..1173ede84e631 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/fields_stats_grid.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import type { FindFileStructureResponse } from '../../../../../../file_upload/common'; import type { DataVisualizerTableState } from '../../../../../common'; import { DataVisualizerTable, ItemIdToExpandedRowMap } from '../stats_table'; -import type { FileBasedFieldVisConfig } from '../stats_table/types/field_vis_config'; +import type { FileBasedFieldVisConfig } from '../../../../../common/types/field_vis_config'; import { FileBasedDataVisualizerExpandedRow } from '../expanded_row'; import { DataVisualizerFieldNamesFilter } from '../field_names_filter'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts index 6c164233bdbc1..9f1ea4af22537 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/filter_fields.ts @@ -9,7 +9,7 @@ import { JOB_FIELD_TYPES } from '../../../../../common'; import type { FileBasedFieldVisConfig, FileBasedUnknownFieldVisConfig, -} from '../stats_table/types/field_vis_config'; +} from '../../../../../common/types/field_vis_config'; export function filterFields( fields: Array, diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx new file mode 100644 index 0000000000000..1d4a685457e25 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.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 { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { DVErrorObject } from '../../../../../index_data_visualizer/utils/error_utils'; + +export const ErrorMessageContent = ({ + fieldName, + error, +}: { + fieldName: string; + error: DVErrorObject; +}) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx index a5db86e0c30a0..d32a8a6dfb907 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/ip_content.tsx @@ -21,12 +21,14 @@ export const IpContent: FC = ({ config, onAddFilter }) => { return ( - + {stats && ( + + )} ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx index 6f946fc1025ed..4fc73f0831dfc 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/text_content.tsx @@ -30,8 +30,9 @@ export const TextContent: FC = ({ config }) => { {numExamples > 0 && } {numExamples === 0 && ( - + { expect(getLegendText(validUnsupportedChartData, 20)).toBe('Chart not supported.'); }); it('should return the chart legend text for empty datasets', () => { - expect(getLegendText(validNumericChartData, 20)).toBe('0 documents contain field.'); + expect(getLegendText(validNumericChartData, 20)).toBe(''); }); it('should return the chart legend text for boolean chart types', () => { const { getByText } = render( @@ -186,7 +186,7 @@ describe('useColumnChart()', () => { ); expect(result.current.data).toStrictEqual([]); - expect(result.current.legendText).toBe('0 documents contain field.'); + expect(result.current.legendText).toBe(''); expect(result.current.xScaleType).toBe('linear'); }); }); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx index 60e1595c64ece..827e4a7f44857 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_row/use_column_chart.tsx @@ -83,9 +83,7 @@ export const getLegendText = (chartData: ChartData, maxChartColumns: number): Le } if (chartData.data.length === 0) { - return i18n.translate('xpack.dataVisualizer.dataGridChart.notEnoughData', { - defaultMessage: `0 documents contain field.`, - }); + return ''; } if (chartData.type === 'boolean') { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx index 4e1c03aa987bd..976afc464a672 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx @@ -33,12 +33,13 @@ import { FieldVisConfig, FileBasedFieldVisConfig, isIndexBasedFieldVisConfig, -} from './types/field_vis_config'; +} from '../../../../../common/types/field_vis_config'; import { FileBasedNumberContentPreview } from '../field_data_row'; import { BooleanContentPreview } from './components/field_data_row'; import { calculateTableColumnsDimensions } from './utils'; import { DistinctValues } from './components/field_data_row/distinct_values'; import { FieldTypeIcon } from '../field_type_icon'; +import './_index.scss'; const FIELD_NAME = 'fieldName'; @@ -54,6 +55,7 @@ interface DataVisualizerTableProps { showPreviewByDefault?: boolean; /** Callback to receive any updates when table or page state is changed **/ onChange?: (update: Partial) => void; + loading?: boolean; } export const DataVisualizerTable = ({ @@ -64,6 +66,7 @@ export const DataVisualizerTable = ({ extendedColumns, showPreviewByDefault, onChange, + loading, }: DataVisualizerTableProps) => { const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [expandAll, setExpandAll] = useState(false); @@ -180,7 +183,7 @@ export const DataVisualizerTable = ({ defaultMessage: 'Type', }), render: (fieldType: JobFieldType) => { - return ; + return ; }, width: dimensions.type, sortable: true, @@ -322,6 +325,13 @@ export const DataVisualizerTable = ({ {(resizeRef) => (
+ message={ + loading + ? i18n.translate('xpack.dataVisualizer.dataGrid.searchingMessage', { + defaultMessage: 'Searching', + }) + : undefined + } className={'dvTable'} items={items} itemId={FIELD_NAME} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts index 94b704764c93b..3d7678c7b60a5 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_data_row.ts @@ -5,8 +5,11 @@ * 2.0. */ -import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config'; import { IndexPatternField } from '../../../../../../../../../src/plugins/data/common'; +import { + FieldVisConfig, + FileBasedFieldVisConfig, +} from '../../../../../../common/types/field_vis_config'; export interface FieldDataRowProps { config: FieldVisConfig | FileBasedFieldVisConfig; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts index 00f8ac0c74eb9..6d9f4d5b86d28 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/index.ts @@ -4,11 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - export type { FieldDataRowProps } from './field_data_row'; export type { FieldVisConfig, FileBasedFieldVisConfig, MetricFieldVisStats, -} from './field_vis_config'; -export { isFileBasedFieldVisConfig, isIndexBasedFieldVisConfig } from './field_vis_config'; +} from '../../../../../../common/types/field_vis_config'; +export { + isFileBasedFieldVisConfig, + isIndexBasedFieldVisConfig, +} from '../../../../../../common/types/field_vis_config'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx index e2793512e23df..c9b4137a0106d 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx @@ -43,7 +43,7 @@ function getPercentLabel(docCount: number, topValuesSampleSize: number): string } export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, onAddFilter }) => { - if (stats === undefined) return null; + if (stats === undefined || !stats.topValues) return null; const { topValues, topValuesSampleSize, @@ -81,11 +81,11 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed, size="xs" label={kibanaFieldFormat(value.key, fieldFormat)} className={classNames('eui-textTruncate', 'topValuesValueLabelContainer')} - valueText={ + valueText={`${value.doc_count}${ progressBarMax !== undefined - ? getPercentLabel(value.doc_count, progressBarMax) - : undefined - } + ? ` (${getPercentLabel(value.doc_count, progressBarMax)})` + : '' + }`} /> {fieldName !== undefined && value.key !== undefined && onAddFilter !== undefined ? ( diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts index 5c0867c7a0745..710ba12313f17 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.test.ts @@ -6,24 +6,23 @@ */ import { JOB_FIELD_TYPES } from '../../../../common'; -import { getJobTypeAriaLabel, jobTypeAriaLabels } from './field_types_utils'; +import { getJobTypeLabel, jobTypeLabels } from './field_types_utils'; describe('field type utils', () => { - describe('getJobTypeAriaLabel: Getting a field type aria label by passing what it is stored in constants', () => { + describe('getJobTypeLabel: Getting a field type aria label by passing what it is stored in constants', () => { test('should returns all JOB_FIELD_TYPES labels exactly as it is for each correct value', () => { const keys = Object.keys(JOB_FIELD_TYPES); const receivedLabels: Record = {}; - const testStorage = jobTypeAriaLabels; - keys.forEach((constant) => { - receivedLabels[constant] = getJobTypeAriaLabel( - JOB_FIELD_TYPES[constant as keyof typeof JOB_FIELD_TYPES] - ); + const testStorage = jobTypeLabels; + keys.forEach((key) => { + const constant = key as keyof typeof JOB_FIELD_TYPES; + receivedLabels[JOB_FIELD_TYPES[constant]] = getJobTypeLabel(JOB_FIELD_TYPES[constant]); }); expect(receivedLabels).toEqual(testStorage); }); test('should returns NULL as JOB_FIELD_TYPES does not contain such a keyword', () => { - expect(getJobTypeAriaLabel('JOB_FIELD_TYPES')).toBe(null); + expect(getJobTypeLabel('JOB_FIELD_TYPES')).toBe(null); }); }); }); diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts index 3e459cd2b079b..1fda7140dbab2 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts @@ -10,40 +10,8 @@ import { JOB_FIELD_TYPES } from '../../../../common'; import type { IndexPatternField } from '../../../../../../../src/plugins/data/common'; import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; -export const jobTypeAriaLabels = { - BOOLEAN: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.booleanTypeAriaLabel', { - defaultMessage: 'boolean type', - }), - DATE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.dateTypeAriaLabel', { - defaultMessage: 'date type', - }), - GEO_POINT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoPointTypeAriaLabel', { - defaultMessage: '{geoPointParam} type', - values: { - geoPointParam: 'geo point', - }, - }), - GEO_SHAPE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeAriaLabel', { - defaultMessage: 'geo shape type', - }), - IP: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel', { - defaultMessage: 'ip type', - }), - KEYWORD: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.keywordTypeAriaLabel', { - defaultMessage: 'keyword type', - }), - NUMBER: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel', { - defaultMessage: 'number type', - }), - HISTOGRAM: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.histogramTypeAriaLabel', { - defaultMessage: 'histogram type', - }), - TEXT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel', { - defaultMessage: 'text type', - }), - UNKNOWN: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.unknownTypeAriaLabel', { - defaultMessage: 'unknown type', - }), +export const getJobTypeLabel = (type: string) => { + return type in jobTypeLabels ? jobTypeLabels[type as keyof typeof jobTypeLabels] : null; }; export const jobTypeLabels = { @@ -88,16 +56,6 @@ export const jobTypeLabels = { }), }; -export const getJobTypeAriaLabel = (type: string) => { - const requestedFieldType = Object.keys(JOB_FIELD_TYPES).find( - (k) => JOB_FIELD_TYPES[k as keyof typeof JOB_FIELD_TYPES] === type - ); - if (requestedFieldType === undefined) { - return null; - } - return jobTypeAriaLabels[requestedFieldType as keyof typeof jobTypeAriaLabels]; -}; - // convert kibana types to ML Job types // this is needed because kibana types only have string and not text and keyword. // and we can't use ES_FIELD_TYPES because it has no NUMBER type diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts b/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts index a1608960a91bc..c259f82d12bfb 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/parse_interval.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { parseInterval } from './parse_interval'; +import { parseInterval } from '../../../../common/utils/parse_interval'; describe('ML parse interval util', () => { test('should correctly parse an interval containing a valid unit and value', () => { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx index e5bd7a0d6f526..ebddd5527f5a2 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx @@ -6,7 +6,6 @@ */ import React, { FC } from 'react'; - import { FormattedMessage } from '@kbn/i18n/react'; import { Query, IndexPattern, TimefilterContract } from 'src/plugins/data/public'; import { EuiButton } from '@elastic/eui'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index cdf4b718a93b7..f528d8378bcd2 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -6,7 +6,6 @@ */ import React, { FC, Fragment, useEffect, useMemo, useState, useCallback, useRef } from 'react'; -import { merge } from 'rxjs'; import { EuiFlexGroup, EuiFlexItem, @@ -16,6 +15,7 @@ import { EuiPageContentHeader, EuiPageContentHeaderSection, EuiPanel, + EuiProgress, EuiSpacer, EuiTitle, } from '@elastic/eui'; @@ -24,12 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Required } from 'utility-types'; import { i18n } from '@kbn/i18n'; import { Filter } from '@kbn/es-query'; -import { - KBN_FIELD_TYPES, - UI_SETTINGS, - Query, - generateFilters, -} from '../../../../../../../../src/plugins/data/public'; +import { Query, generateFilters } from '../../../../../../../../src/plugins/data/public'; import { FullTimeRangeSelector } from '../full_time_range_selector'; import { usePageUrlState, useUrlState } from '../../../common/util/url_state'; import { @@ -37,39 +32,29 @@ import { ItemIdToExpandedRowMap, } from '../../../common/components/stats_table'; import { FieldVisConfig } from '../../../common/components/stats_table/types'; -import type { - MetricFieldsStats, - TotalFieldsStats, -} from '../../../common/components/stats_table/components/field_count_stats'; +import type { TotalFieldsStats } from '../../../common/components/stats_table/components/field_count_stats'; import { OverallStats } from '../../types/overall_stats'; import { getActions } from '../../../common/components/field_data_row/action_menu'; import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row'; import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer'; import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../types/combined_query'; -import { - FieldRequestConfig, - JobFieldType, - SavedSearchSavedObject, -} from '../../../../../common/types'; +import { JobFieldType, SavedSearchSavedObject } from '../../../../../common/types'; import { useDataVisualizerKibana } from '../../../kibana_context'; import { FieldCountPanel } from '../../../common/components/field_count_panel'; import { DocumentCountContent } from '../../../common/components/document_count_content'; -import { DataLoader } from '../../data_loader/data_loader'; -import { JOB_FIELD_TYPES, OMIT_FIELDS } from '../../../../../common'; -import { useTimefilter } from '../../hooks/use_time_filter'; +import { OMIT_FIELDS } from '../../../../../common'; import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; import { SearchPanel } from '../search_panel'; import { ActionsPanel } from '../actions_panel'; import { DatePickerWrapper } from '../../../common/components/date_picker_wrapper'; -import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service'; import { HelpMenu } from '../../../common/components/help_menu'; -import { TimeBuckets } from '../../services/time_buckets'; -import { createMergedEsQuery, getEsQueryFromSavedSearch } from '../../utils/saved_search_utils'; +import { createMergedEsQuery } from '../../utils/saved_search_utils'; import { DataVisualizerIndexPatternManagement } from '../index_pattern_management'; import { ResultLink } from '../../../common/components/results_links'; -import { extractErrorProperties } from '../../utils/error_utils'; import { IndexPatternField, IndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; +import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable'; import './_index.scss'; interface DataVisualizerPageState { @@ -155,61 +140,14 @@ export const IndexDataVisualizerView: FC = (dataVi } }, [dataVisualizerProps?.currentSavedSearch]); - useEffect(() => { - return () => { - // When navigating away from the data view - // Reset all previously set filters - // to make sure new page doesn't have unrelated filters - data.query.filterManager.removeAll(); - }; - }, [currentIndexPattern.id, data.query.filterManager]); - - const getTimeBuckets = useCallback(() => { - return new TimeBuckets({ - [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - dateFormat: uiSettings.get('dateFormat'), - 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), - }); - }, [uiSettings]); - - const timefilter = useTimefilter({ - timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined, - autoRefreshSelector: true, - }); - - const dataLoader = useMemo( - () => new DataLoader(currentIndexPattern, toasts), - [currentIndexPattern, toasts] - ); - - useEffect(() => { - if (globalState?.time !== undefined) { - timefilter.setTime({ - from: globalState.time.from, - to: globalState.time.to, - }); - setLastRefresh(Date.now()); - } - }, [globalState, timefilter]); - - useEffect(() => { - if (globalState?.refreshInterval !== undefined) { - timefilter.setRefreshInterval(globalState.refreshInterval); - setLastRefresh(Date.now()); - } - }, [globalState, timefilter]); - - const [lastRefresh, setLastRefresh] = useState(0); - useEffect(() => { if (!currentIndexPattern.isTimeBased()) { toasts.addWarning({ title: i18n.translate( - 'xpack.dataVisualizer.index.dataViewNotBasedOnTimeSeriesNotificationTitle', + 'xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle', { - defaultMessage: 'The data view {dataViewTitle} is not based on a time series', - values: { dataViewTitle: currentIndexPattern.title }, + defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series', + values: { indexPatternTitle: currentIndexPattern.title }, } ), text: i18n.translate( @@ -225,7 +163,7 @@ export const IndexDataVisualizerView: FC = (dataVi const indexPatternFields: IndexPatternField[] = currentIndexPattern.fields; const fieldTypes = useMemo(() => { - // Obtain the list of non metric field types which appear in the data view. + // Obtain the list of non metric field types which appear in the index pattern. const indexedFieldTypes: JobFieldType[] = []; indexPatternFields.forEach((field) => { if (!OMIT_FIELDS.includes(field.name) && field.scripted !== true) { @@ -238,35 +176,6 @@ export const IndexDataVisualizerView: FC = (dataVi return indexedFieldTypes.sort(); }, [indexPatternFields]); - const defaults = getDefaultPageState(); - - const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { - const searchData = getEsQueryFromSavedSearch({ - indexPattern: currentIndexPattern, - uiSettings, - savedSearch: currentSavedSearch, - filterManager: data.query.filterManager, - }); - - if (searchData === undefined || dataVisualizerListState.searchString !== '') { - if (dataVisualizerListState.filters) { - data.query.filterManager.setFilters(dataVisualizerListState.filters); - } - return { - searchQuery: dataVisualizerListState.searchQuery, - searchString: dataVisualizerListState.searchString, - searchQueryLanguage: dataVisualizerListState.searchQueryLanguage, - }; - } else { - return { - searchQuery: searchData.searchQuery, - searchString: searchData.searchString, - searchQueryLanguage: searchData.queryLanguage, - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentSavedSearch, currentIndexPattern, dataVisualizerListState, data.query]); - const setSearchParams = useCallback( (searchParams: { searchQuery: Query['query']; @@ -275,7 +184,7 @@ export const IndexDataVisualizerView: FC = (dataVi filters: Filter[]; }) => { // When the user loads saved search and then clear or modify the query - // we should remove the saved search and replace it with the data view id + // we should remove the saved search and replace it with the index pattern id if (currentSavedSearch !== null) { setCurrentSavedSearch(null); } @@ -318,15 +227,58 @@ export const IndexDataVisualizerView: FC = (dataVi }); }; - const [overallStats, setOverallStats] = useState(defaults.overallStats); + const input: DataVisualizerGridInput = useMemo(() => { + return { + indexPattern: currentIndexPattern, + savedSearch: currentSavedSearch, + visibleFieldNames, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentIndexPattern.id, currentSavedSearch?.id, visibleFieldNames]); + + const { + configs, + searchQueryLanguage, + searchString, + overallStats, + searchQuery, + documentCountStats, + metricsStats, + timefilter, + setLastRefresh, + progress, + } = useDataVisualizerGridData(input, dataVisualizerListState, setGlobalState); + + useEffect(() => { + return () => { + // When navigating away from the index pattern + // Reset all previously set filters + // to make sure new page doesn't have unrelated filters + data.query.filterManager.removeAll(); + }; + }, [currentIndexPattern.id, data.query.filterManager]); + + useEffect(() => { + // Force refresh on index pattern change + setLastRefresh(Date.now()); + }, [currentIndexPattern.id, setLastRefresh]); - const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats); - const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); - const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded); - const [metricsStats, setMetricsStats] = useState(); + useEffect(() => { + if (globalState?.time !== undefined) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(globalState?.time), timefilter]); - const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); - const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); + useEffect(() => { + if (globalState?.refreshInterval !== undefined) { + timefilter.setRefreshInterval(globalState.refreshInterval); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(globalState?.refreshInterval), timefilter]); const onAddFilter = useCallback( (field: IndexPatternField | string, values: string, operation: '+' | '-') => { @@ -374,422 +326,8 @@ export const IndexDataVisualizerView: FC = (dataVi ] ); - useEffect(() => { - const timeUpdateSubscription = merge( - timefilter.getTimeUpdate$(), - dataVisualizerRefresh$ - ).subscribe(() => { - setGlobalState({ - time: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - }); - setLastRefresh(Date.now()); - }); - return () => { - timeUpdateSubscription.unsubscribe(); - }; - }); - - useEffect(() => { - loadOverallStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, samplerShardSize, lastRefresh]); - - useEffect(() => { - createMetricCards(); - createNonMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [overallStats, showEmptyFields]); - - useEffect(() => { - loadMetricFieldStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricConfigs]); - - useEffect(() => { - loadNonMetricFieldStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nonMetricConfigs]); - - useEffect(() => { - createMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricsLoaded]); - - useEffect(() => { - createNonMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nonMetricsLoaded]); - - async function loadOverallStats() { - const tf = timefilter as any; - let earliest; - let latest; - - const activeBounds = tf.getActiveBounds(); - - if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) { - return; - } - - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = activeBounds.min.valueOf(); - latest = activeBounds.max.valueOf(); - } - - try { - const allStats = await dataLoader.loadOverallData( - searchQuery, - samplerShardSize, - earliest, - latest - ); - // Because load overall stats perform queries in batches - // there could be multiple errors - if (Array.isArray(allStats.errors) && allStats.errors.length > 0) { - allStats.errors.forEach((err: any) => { - dataLoader.displayError(extractErrorProperties(err)); - }); - } - setOverallStats(allStats); - } catch (err) { - dataLoader.displayError(err.body ?? err); - } - } - - async function loadMetricFieldStats() { - // Only request data for fields that exist in documents. - if (metricConfigs.length === 0) { - return; - } - - const configsToLoad = metricConfigs.filter( - (config) => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - // Obtain the interval to use for date histogram aggregations - // (such as the document count chart). Aim for 75 bars. - const buckets = getTimeBuckets(); - - const tf = timefilter as any; - let earliest: number | undefined; - let latest: number | undefined; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - const bounds = tf.getActiveBounds(); - const BAR_TARGET = 75; - buckets.setInterval('auto'); - buckets.setBounds(bounds); - buckets.setBarTarget(BAR_TARGET); - const aggInterval = buckets.getInterval(); - - try { - const metricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existMetricFields, - aggInterval.asMilliseconds() - ); - - // Add the metric stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - metricConfigs.forEach((config) => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - configWithStats.loading = false; - configs.push(configWithStats); - } else { - // Document count card. - configWithStats.stats = metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === undefined - ); - - if (configWithStats.stats !== undefined) { - // Add earliest / latest of timefilter for setting x axis domain. - configWithStats.stats.timeRangeEarliest = earliest; - configWithStats.stats.timeRangeLatest = latest; - } - setDocumentCountStats(configWithStats); - } - }); - - setMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - async function loadNonMetricFieldStats() { - // Only request data for fields that exist in documents. - if (nonMetricConfigs.length === 0) { - return; - } - - const configsToLoad = nonMetricConfigs.filter( - (config) => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - const tf = timefilter as any; - let earliest; - let latest; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - try { - const nonMetricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existNonMetricFields - ); - - // Add the field stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - nonMetricConfigs.forEach((config) => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...nonMetricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - } - configWithStats.loading = false; - configs.push(configWithStats); - }); - - setNonMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - const createMetricCards = useCallback(() => { - const configs: FieldVisConfig[] = []; - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - - const allMetricFields = indexPatternFields.filter((f) => { - return ( - f.type === KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - const metricExistsFields = allMetricFields.filter((f) => { - return aggregatableExistsFields.find((existsF) => { - return existsF.fieldName === f.spec.name; - }); - }); - - // Add a config for 'document count', identified by no field name if indexpattern is time based. - if (currentIndexPattern.timeFieldName !== undefined) { - configs.push({ - type: JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - loading: true, - aggregatable: true, - }); - } - - if (metricsLoaded === false) { - setMetricsLoaded(true); - return; - } - - let aggregatableFields: any[] = overallStats.aggregatableExistsFields; - if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) { - aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); - } - - const metricFieldsToShow = - metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields; - - metricFieldsToShow.forEach((field) => { - const fieldData = aggregatableFields.find((f) => { - return f.fieldName === field.spec.name; - }); - - const metricConfig: FieldVisConfig = { - ...(fieldData ? fieldData : {}), - fieldFormat: currentIndexPattern.getFormatterForField(field), - type: JOB_FIELD_TYPES.NUMBER, - loading: true, - aggregatable: true, - deletable: field.runtimeField !== undefined, - }; - if (field.displayName !== metricConfig.fieldName) { - metricConfig.displayName = field.displayName; - } - - configs.push(metricConfig); - }); - - setMetricsStats({ - totalMetricFieldsCount: allMetricFields.length, - visibleMetricsCount: metricFieldsToShow.length, - }); - setMetricConfigs(configs); - }, [ - currentIndexPattern, - dataLoader, - indexPatternFields, - metricsLoaded, - overallStats, - showEmptyFields, - ]); - - const createNonMetricCards = useCallback(() => { - const allNonMetricFields = indexPatternFields.filter((f) => { - return ( - f.type !== KBN_FIELD_TYPES.NUMBER && - f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true - ); - }); - // Obtain the list of all non-metric fields which appear in documents - // (aggregatable or not aggregatable). - const populatedNonMetricFields: any[] = []; // Kibana data view non metric fields. - let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats. - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || []; - - allNonMetricFields.forEach((f) => { - const checkAggregatableField = aggregatableExistsFields.find( - (existsField) => existsField.fieldName === f.spec.name - ); - - if (checkAggregatableField !== undefined) { - populatedNonMetricFields.push(f); - nonMetricFieldData.push(checkAggregatableField); - } else { - const checkNonAggregatableField = nonAggregatableExistsFields.find( - (existsField) => existsField.fieldName === f.spec.name - ); - - if (checkNonAggregatableField !== undefined) { - populatedNonMetricFields.push(f); - nonMetricFieldData.push(checkNonAggregatableField); - } - } - }); - - if (nonMetricsLoaded === false) { - setNonMetricsLoaded(true); - return; - } - - if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) { - // Combine the field data obtained from Elasticsearch into a single array. - nonMetricFieldData = nonMetricFieldData.concat( - overallStats.aggregatableNotExistsFields, - overallStats.nonAggregatableNotExistsFields - ); - } - - const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields; - - const configs: FieldVisConfig[] = []; - - nonMetricFieldsToShow.forEach((field) => { - const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name); - - const nonMetricConfig = { - ...(fieldData ? fieldData : {}), - fieldFormat: currentIndexPattern.getFormatterForField(field), - aggregatable: field.aggregatable, - scripted: field.scripted, - loading: fieldData?.existsInDocs, - deletable: field.runtimeField !== undefined, - }; - - // Map the field type from the Kibana data view to the field type - // used in the data visualizer. - const dataVisualizerType = kbnTypeToJobType(field); - if (dataVisualizerType !== undefined) { - nonMetricConfig.type = dataVisualizerType; - } else { - // Add a flag to indicate that this is one of the 'other' Kibana - // field types that do not yet have a specific card type. - nonMetricConfig.type = field.type; - nonMetricConfig.isUnsupportedType = true; - } - - if (field.displayName !== nonMetricConfig.fieldName) { - nonMetricConfig.displayName = field.displayName; - } - - configs.push(nonMetricConfig); - }); - - setNonMetricConfigs(configs); - }, [ - currentIndexPattern, - dataLoader, - indexPatternFields, - nonMetricsLoaded, - overallStats, - showEmptyFields, - ]); - const wizardPanelWidth = '280px'; - const configs = useMemo(() => { - let combinedConfigs = [...nonMetricConfigs, ...metricConfigs]; - if (visibleFieldTypes && visibleFieldTypes.length > 0) { - combinedConfigs = combinedConfigs.filter( - (config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1 - ); - } - if (visibleFieldNames && visibleFieldNames.length > 0) { - combinedConfigs = combinedConfigs.filter( - (config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1 - ); - } - - return combinedConfigs; - }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]); - const fieldsCountStats: TotalFieldsStats | undefined = useMemo(() => { let _visibleFieldsCount = 0; let _totalFieldsCount = 0; @@ -923,7 +461,7 @@ export const IndexDataVisualizerView: FC = (dataVi {overallStats?.totalCount !== undefined && ( @@ -953,12 +491,14 @@ export const IndexDataVisualizerView: FC = (dataVi metricsStats={metricsStats} /> + items={configs} pageState={dataVisualizerListState} updatePageState={setDataVisualizerListState} getItemIdToExpandedRowMap={getItemIdToExpandedRowMap} extendedColumns={extendedColumns} + loading={progress < 100} /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx index 7e86425c0a891..ee54683b08435 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx @@ -29,7 +29,7 @@ export const DataVisualizerFieldTypeFilter: FC<{ {label} {indexedFieldName && ( - + )} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx index f55114ca36d78..25ed13121fc34 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx @@ -22,6 +22,7 @@ import { SearchQueryLanguage } from '../../types/combined_query'; import { useDataVisualizerKibana } from '../../../kibana_context'; import './_index.scss'; import { createMergedEsQuery } from '../../utils/saved_search_utils'; +import { OverallStats } from '../../types/overall_stats'; interface Props { indexPattern: IndexPattern; searchString: Query['query']; @@ -29,7 +30,7 @@ interface Props { searchQueryLanguage: SearchQueryLanguage; samplerShardSize: number; setSamplerShardSize(s: number): void; - overallStats: any; + overallStats: OverallStats; indexedFieldTypes: JobFieldType[]; setVisibleFieldTypes(q: string[]): void; visibleFieldTypes: string[]; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts deleted file mode 100644 index e0a2852a57b29..0000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts +++ /dev/null @@ -1,142 +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. - */ - -// Maximum number of examples to obtain for text type fields. -import { CoreSetup } from 'kibana/public'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { i18n } from '@kbn/i18n'; -import { IndexPattern } from '../../../../../../../src/plugins/data/common'; -import { NON_AGGREGATABLE_FIELD_TYPES, OMIT_FIELDS } from '../../../../common/constants'; -import { FieldRequestConfig } from '../../../../common/types'; -import { getVisualizerFieldStats, getVisualizerOverallStats } from '../services/visualizer_stats'; - -type IndexPatternTitle = string; -type SavedSearchQuery = Record | null | undefined; - -const MAX_EXAMPLES_DEFAULT: number = 10; - -export class DataLoader { - private _indexPattern: IndexPattern; - private _runtimeMappings: estypes.MappingRuntimeFields; - private _indexPatternTitle: IndexPatternTitle = ''; - private _maxExamples: number = MAX_EXAMPLES_DEFAULT; - private _toastNotifications: CoreSetup['notifications']['toasts']; - - constructor( - indexPattern: IndexPattern, - toastNotifications: CoreSetup['notifications']['toasts'] - ) { - this._indexPattern = indexPattern; - this._runtimeMappings = this._indexPattern.getComputedFields() - .runtimeFields as estypes.MappingRuntimeFields; - this._indexPatternTitle = indexPattern.title; - this._toastNotifications = toastNotifications; - } - - async loadOverallData( - query: string | SavedSearchQuery, - samplerShardSize: number, - earliest: number | undefined, - latest: number | undefined - ): Promise { - const aggregatableFields: string[] = []; - const nonAggregatableFields: string[] = []; - this._indexPattern.fields.forEach((field) => { - const fieldName = field.displayName !== undefined ? field.displayName : field.name; - if (this.isDisplayField(fieldName) === true) { - if (field.aggregatable === true && !NON_AGGREGATABLE_FIELD_TYPES.has(field.type)) { - aggregatableFields.push(field.name); - } else { - nonAggregatableFields.push(field.name); - } - } - }); - - // Need to find: - // 1. List of aggregatable fields that do exist in docs - // 2. List of aggregatable fields that do not exist in docs - // 3. List of non-aggregatable fields that do exist in docs. - // 4. List of non-aggregatable fields that do not exist in docs. - const stats = await getVisualizerOverallStats({ - indexPatternTitle: this._indexPatternTitle, - query, - timeFieldName: this._indexPattern.timeFieldName, - samplerShardSize, - earliest, - latest, - aggregatableFields, - nonAggregatableFields, - runtimeMappings: this._runtimeMappings, - }); - - return stats; - } - - async loadFieldStats( - query: string | SavedSearchQuery, - samplerShardSize: number, - earliest: number | undefined, - latest: number | undefined, - fields: FieldRequestConfig[], - interval?: number - ): Promise { - const stats = await getVisualizerFieldStats({ - indexPatternTitle: this._indexPatternTitle, - query, - timeFieldName: this._indexPattern.timeFieldName, - earliest, - latest, - samplerShardSize, - interval, - fields, - maxExamples: this._maxExamples, - runtimeMappings: this._runtimeMappings, - }); - - return stats; - } - - displayError(err: any) { - if (err.statusCode === 500) { - this._toastNotifications.addError(err, { - title: i18n.translate('xpack.dataVisualizer.index.dataLoader.internalServerErrorMessage', { - defaultMessage: - 'Error loading data in index {index}. {message}. ' + - 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', - values: { - index: this._indexPattern.title, - message: err.error ?? err.message, - }, - }), - }); - } else { - this._toastNotifications.addError(err, { - title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage', { - defaultMessage: 'Error loading data in index {index}. {message}.', - values: { - index: this._indexPattern.title, - message: err.error ?? err.message, - }, - }), - }); - } - } - - public set maxExamples(max: number) { - this._maxExamples = max; - } - - public get maxExamples(): number { - return this._maxExamples; - } - - // Returns whether the field with the specified name should be displayed, - // as certain fields such as _id and _source should be omitted from the view. - public isDisplayField(fieldName: string): boolean { - return !OMIT_FIELDS.includes(fieldName); - } -} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx index f59225b1c019f..0391d5ae5d5d5 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/grid_embeddable.tsx @@ -8,7 +8,7 @@ import { Observable, Subject } from 'rxjs'; import { CoreStart } from 'kibana/public'; import ReactDOM from 'react-dom'; -import React, { Suspense, useCallback, useState } from 'react'; +import React, { Suspense, useCallback, useEffect, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { Filter } from '@kbn/es-query'; @@ -36,15 +36,15 @@ import { } from '../../../common/components/stats_table'; import { FieldVisConfig } from '../../../common/components/stats_table/types'; import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view'; -import { DataVisualizerTableState } from '../../../../../common'; +import { DataVisualizerTableState, SavedSearchSavedObject } from '../../../../../common'; import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row'; -import { useDataVisualizerGridData } from './use_data_visualizer_grid_data'; +import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies]; -export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { +export interface DataVisualizerGridInput { indexPattern: IndexPattern; - savedSearch?: SavedSearch; + savedSearch?: SavedSearch | SavedSearchSavedObject | null; query?: Query; visibleFieldNames?: string[]; filters?: Filter[]; @@ -54,6 +54,7 @@ export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { */ onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; } +export type DataVisualizerGridEmbeddableInput = EmbeddableInput & DataVisualizerGridInput; export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput; export type IDataVisualizerGridEmbeddable = typeof DataVisualizerGridEmbeddable; @@ -79,8 +80,13 @@ export const EmbeddableWrapper = ({ }, [dataVisualizerListState, onOutputChange] ); - const { configs, searchQueryLanguage, searchString, extendedColumns, loaded } = + const { configs, searchQueryLanguage, searchString, extendedColumns, progress, setLastRefresh } = useDataVisualizerGridData(input, dataVisualizerListState); + + useEffect(() => { + setLastRefresh(Date.now()); + }, [input?.lastReloadRequestTime, setLastRefresh]); + const getItemIdToExpandedRowMap = useCallback( function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap { return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => { @@ -101,13 +107,7 @@ export const EmbeddableWrapper = ({ [input, searchQueryLanguage, searchString] ); - if ( - loaded && - (configs.length === 0 || - // FIXME: Configs might have a placeholder document count stats field - // This will be removed in the future - (configs.length === 1 && configs[0].fieldName === undefined)) - ) { + if (progress === 100 && configs.length === 0) { return (
); }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts similarity index 52% rename from x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts rename to x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts index fc0fc7a2134b4..e6e7a96e0329f 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/use_data_visualizer_grid_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts @@ -10,39 +10,54 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { merge } from 'rxjs'; import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types'; import { i18n } from '@kbn/i18n'; -import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state'; -import { useDataVisualizerKibana } from '../../../kibana_context'; -import { getEsQueryFromSavedSearch } from '../../utils/saved_search_utils'; -import { MetricFieldsStats } from '../../../common/components/stats_table/components/field_count_stats'; -import { DataLoader } from '../../data_loader/data_loader'; -import { useTimefilter } from '../../hooks/use_time_filter'; -import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service'; -import { TimeBuckets } from '../../services/time_buckets'; +import { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state'; +import { useDataVisualizerKibana } from '../../kibana_context'; +import { getEsQueryFromSavedSearch } from '../utils/saved_search_utils'; +import { MetricFieldsStats } from '../../common/components/stats_table/components/field_count_stats'; +import { useTimefilter } from './use_time_filter'; +import { dataVisualizerRefresh$ } from '../services/timefilter_refresh_service'; +import { TimeBuckets } from '../../../../common/services/time_buckets'; import { DataViewField, KBN_FIELD_TYPES, UI_SETTINGS, -} from '../../../../../../../../src/plugins/data/common'; -import { extractErrorProperties } from '../../utils/error_utils'; -import { FieldVisConfig } from '../../../common/components/stats_table/types'; -import { FieldRequestConfig, JOB_FIELD_TYPES } from '../../../../../common'; -import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; -import { getActions } from '../../../common/components/field_data_row/action_menu'; -import { DataVisualizerGridEmbeddableInput } from './grid_embeddable'; -import { getDefaultPageState } from '../../components/index_data_visualizer_view/index_data_visualizer_view'; +} from '../../../../../../../src/plugins/data/common'; +import { FieldVisConfig } from '../../common/components/stats_table/types'; +import { + FieldRequestConfig, + JOB_FIELD_TYPES, + JobFieldType, + NON_AGGREGATABLE_FIELD_TYPES, + OMIT_FIELDS, +} from '../../../../common'; +import { kbnTypeToJobType } from '../../common/util/field_types_utils'; +import { getActions } from '../../common/components/field_data_row/action_menu'; +import { DataVisualizerGridInput } from '../embeddables/grid_embeddable/grid_embeddable'; +import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view'; +import { useFieldStatsSearchStrategy } from './use_field_stats'; +import { useOverallStats } from './use_overall_stats'; +import { OverallStatsSearchStrategyParams } from '../../../../common/types/field_stats'; +import { Dictionary } from '../../common/util/url_state'; +import { AggregatableField, NonAggregatableField } from '../types/overall_stats'; const defaults = getDefaultPageState(); +function isDisplayField(fieldName: string): boolean { + return !OMIT_FIELDS.includes(fieldName); +} + export const useDataVisualizerGridData = ( - input: DataVisualizerGridEmbeddableInput, - dataVisualizerListState: Required + input: DataVisualizerGridInput, + dataVisualizerListState: Required, + onUpdate?: (params: Dictionary) => void ) => { const { services } = useDataVisualizerKibana(); - const { notifications, uiSettings } = services; - const { toasts } = notifications; + const { uiSettings, data } = services; const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState; + const dataVisualizerListStateRef = useRef(dataVisualizerListState); const [lastRefresh, setLastRefresh] = useState(0); + const [searchSessionId, setSearchSessionId] = useState(); const { currentSavedSearch, @@ -61,6 +76,7 @@ export const useDataVisualizerGridData = ( [input] ); + /** Prepare required params to pass to search strategy **/ const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { const searchData = getEsQueryFromSavedSearch({ indexPattern: currentIndexPattern, @@ -68,9 +84,13 @@ export const useDataVisualizerGridData = ( savedSearch: currentSavedSearch, query: currentQuery, filters: currentFilters, + filterManager: data.query.filterManager, }); if (searchData === undefined || dataVisualizerListState.searchString !== '') { + if (dataVisualizerListState.filters) { + data.query.filterManager.setFilters(dataVisualizerListState.filters); + } return { searchQuery: dataVisualizerListState.searchQuery, searchString: dataVisualizerListState.searchString, @@ -85,16 +105,40 @@ export const useDataVisualizerGridData = ( } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - currentSavedSearch, - currentIndexPattern, - dataVisualizerListState, - currentQuery, - currentFilters, + currentSavedSearch?.id, + currentIndexPattern.id, + dataVisualizerListState.searchString, + dataVisualizerListState.searchQueryLanguage, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify({ + searchQuery: dataVisualizerListState.searchQuery, + currentQuery, + currentFilters, + }), + lastRefresh, ]); - const [overallStats, setOverallStats] = useState(defaults.overallStats); + useEffect(() => { + const currentSearchSessionId = data.search?.session?.getSessionId(); + if (currentSearchSessionId !== undefined) { + setSearchSessionId(currentSearchSessionId); + } + }, [data]); + + const _timeBuckets = useMemo(() => { + return new TimeBuckets({ + [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); + }, [uiSettings]); + + const timefilter = useTimefilter({ + timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined, + autoRefreshSelector: true, + }); - const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats); const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs); const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded); const [metricsStats, setMetricsStats] = useState(); @@ -102,21 +146,134 @@ export const useDataVisualizerGridData = ( const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); - const dataLoader = useMemo( - () => new DataLoader(currentIndexPattern, toasts), - [currentIndexPattern, toasts] + /** Search strategy **/ + const fieldStatsRequest: OverallStatsSearchStrategyParams | undefined = useMemo( + () => { + // Obtain the interval to use for date histogram aggregations + // (such as the document count chart). Aim for 75 bars. + const buckets = _timeBuckets; + + const tf = timefilter; + + if (!buckets || !tf || !currentIndexPattern) return; + + const activeBounds = tf.getActiveBounds(); + + let earliest: number | undefined; + let latest: number | undefined; + if (activeBounds !== undefined && currentIndexPattern.timeFieldName !== undefined) { + earliest = activeBounds.min?.valueOf(); + latest = activeBounds.max?.valueOf(); + } + + const bounds = tf.getActiveBounds(); + const BAR_TARGET = 75; + buckets.setInterval('auto'); + + if (bounds) { + buckets.setBounds(bounds); + buckets.setBarTarget(BAR_TARGET); + } + + const aggInterval = buckets.getInterval(); + + const aggregatableFields: string[] = []; + const nonAggregatableFields: string[] = []; + currentIndexPattern.fields.forEach((field) => { + const fieldName = field.displayName !== undefined ? field.displayName : field.name; + if (!OMIT_FIELDS.includes(fieldName)) { + if (field.aggregatable === true && !NON_AGGREGATABLE_FIELD_TYPES.has(field.type)) { + aggregatableFields.push(field.name); + } else { + nonAggregatableFields.push(field.name); + } + } + }); + return { + earliest, + latest, + aggInterval, + intervalMs: aggInterval?.asMilliseconds(), + searchQuery, + samplerShardSize, + sessionId: searchSessionId, + index: currentIndexPattern.title, + timeFieldName: currentIndexPattern.timeFieldName, + runtimeFieldMap: currentIndexPattern.getComputedFields().runtimeFields, + aggregatableFields, + nonAggregatableFields, + }; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + _timeBuckets, + timefilter, + currentIndexPattern.id, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(searchQuery), + samplerShardSize, + searchSessionId, + lastRefresh, + ] + ); + + const { overallStats, progress: overallStatsProgress } = useOverallStats( + fieldStatsRequest, + lastRefresh ); - const timefilter = useTimefilter({ - timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined, - autoRefreshSelector: true, - }); + const configsWithoutStats = useMemo(() => { + if (overallStatsProgress.loaded < 100) return; + const existMetricFields = metricConfigs + .map((config) => { + if (config.existsInDocs === false) return; + return { + fieldName: config.fieldName, + type: config.type, + cardinality: config.stats?.cardinality ?? 0, + }; + }) + .filter((c) => c !== undefined) as FieldRequestConfig[]; + + // Pass the field name, type and cardinality in the request. + // Top values will be obtained on a sample if cardinality > 100000. + const existNonMetricFields: FieldRequestConfig[] = nonMetricConfigs + .map((config) => { + if (config.existsInDocs === false) return; + return { + fieldName: config.fieldName, + type: config.type, + cardinality: config.stats?.cardinality ?? 0, + }; + }) + .filter((c) => c !== undefined) as FieldRequestConfig[]; + + return { metricConfigs: existMetricFields, nonMetricConfigs: existNonMetricFields }; + }, [metricConfigs, nonMetricConfigs, overallStatsProgress.loaded]); + + const strategyResponse = useFieldStatsSearchStrategy( + fieldStatsRequest, + configsWithoutStats, + dataVisualizerListStateRef.current + ); + + const combinedProgress = useMemo( + () => overallStatsProgress.loaded * 0.2 + strategyResponse.progress.loaded * 0.8, + [overallStatsProgress.loaded, strategyResponse.progress.loaded] + ); useEffect(() => { const timeUpdateSubscription = merge( timefilter.getTimeUpdate$(), + timefilter.getAutoRefreshFetch$(), dataVisualizerRefresh$ ).subscribe(() => { + if (onUpdate) { + onUpdate({ + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }); + } setLastRefresh(Date.now()); }); return () => { @@ -124,65 +281,21 @@ export const useDataVisualizerGridData = ( }; }); - const getTimeBuckets = useCallback(() => { - return new TimeBuckets({ - [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - [UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - dateFormat: uiSettings.get('dateFormat'), - 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), - }); - }, [uiSettings]); - const indexPatternFields: DataViewField[] = useMemo( () => currentIndexPattern.fields, [currentIndexPattern] ); - async function loadOverallStats() { - const tf = timefilter as any; - let earliest; - let latest; - - const activeBounds = tf.getActiveBounds(); - - if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) { - return; - } - - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = activeBounds.min.valueOf(); - latest = activeBounds.max.valueOf(); - } - - try { - const allStats = await dataLoader.loadOverallData( - searchQuery, - samplerShardSize, - earliest, - latest - ); - // Because load overall stats perform queries in batches - // there could be multiple errors - if (Array.isArray(allStats.errors) && allStats.errors.length > 0) { - allStats.errors.forEach((err: any) => { - dataLoader.displayError(extractErrorProperties(err)); - }); - } - setOverallStats(allStats); - } catch (err) { - dataLoader.displayError(err.body ?? err); - } - } - const createMetricCards = useCallback(() => { const configs: FieldVisConfig[] = []; - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; + const aggregatableExistsFields: AggregatableField[] = + overallStats.aggregatableExistsFields || []; const allMetricFields = indexPatternFields.filter((f) => { return ( f.type === KBN_FIELD_TYPES.NUMBER && f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true + isDisplayField(f.displayName) === true ); }); const metricExistsFields = allMetricFields.filter((f) => { @@ -191,22 +304,12 @@ export const useDataVisualizerGridData = ( }); }); - // Add a config for 'document count', identified by no field name if indexpattern is time based. - if (currentIndexPattern.timeFieldName !== undefined) { - configs.push({ - type: JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - loading: true, - aggregatable: true, - }); - } - if (metricsLoaded === false) { setMetricsLoaded(true); return; } - let aggregatableFields: any[] = overallStats.aggregatableExistsFields; + let aggregatableFields: AggregatableField[] = overallStats.aggregatableExistsFields; if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) { aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields); } @@ -218,9 +321,10 @@ export const useDataVisualizerGridData = ( const fieldData = aggregatableFields.find((f) => { return f.fieldName === field.spec.name; }); + if (!fieldData) return; const metricConfig: FieldVisConfig = { - ...(fieldData ? fieldData : {}), + ...fieldData, fieldFormat: currentIndexPattern.getFormatterForField(field), type: JOB_FIELD_TYPES.NUMBER, loading: true, @@ -239,29 +343,24 @@ export const useDataVisualizerGridData = ( visibleMetricsCount: metricFieldsToShow.length, }); setMetricConfigs(configs); - }, [ - currentIndexPattern, - dataLoader, - indexPatternFields, - metricsLoaded, - overallStats, - showEmptyFields, - ]); + }, [currentIndexPattern, indexPatternFields, metricsLoaded, overallStats, showEmptyFields]); const createNonMetricCards = useCallback(() => { const allNonMetricFields = indexPatternFields.filter((f) => { return ( f.type !== KBN_FIELD_TYPES.NUMBER && f.displayName !== undefined && - dataLoader.isDisplayField(f.displayName) === true + isDisplayField(f.displayName) === true ); }); // Obtain the list of all non-metric fields which appear in documents // (aggregatable or not aggregatable). - const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields. - let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats. - const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || []; - const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || []; + const populatedNonMetricFields: DataViewField[] = []; // Kibana index pattern non metric fields. + let nonMetricFieldData: Array = []; // Basic non metric field data loaded from requesting overall stats. + const aggregatableExistsFields: AggregatableField[] = + overallStats.aggregatableExistsFields || []; + const nonAggregatableExistsFields: NonAggregatableField[] = + overallStats.nonAggregatableExistsFields || []; allNonMetricFields.forEach((f) => { const checkAggregatableField = aggregatableExistsFields.find( @@ -303,12 +402,11 @@ export const useDataVisualizerGridData = ( nonMetricFieldsToShow.forEach((field) => { const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name); - const nonMetricConfig = { + const nonMetricConfig: Partial = { ...(fieldData ? fieldData : {}), fieldFormat: currentIndexPattern.getFormatterForField(field), aggregatable: field.aggregatable, - scripted: field.scripted, - loading: fieldData?.existsInDocs, + loading: fieldData?.existsInDocs ?? true, deletable: field.runtimeField !== undefined, }; @@ -320,7 +418,7 @@ export const useDataVisualizerGridData = ( } else { // Add a flag to indicate that this is one of the 'other' Kibana // field types that do not yet have a specific card type. - nonMetricConfig.type = field.type; + nonMetricConfig.type = field.type as JobFieldType; nonMetricConfig.isUnsupportedType = true; } @@ -328,171 +426,11 @@ export const useDataVisualizerGridData = ( nonMetricConfig.displayName = field.displayName; } - configs.push(nonMetricConfig); + configs.push(nonMetricConfig as FieldVisConfig); }); setNonMetricConfigs(configs); - }, [ - currentIndexPattern, - dataLoader, - indexPatternFields, - nonMetricsLoaded, - overallStats, - showEmptyFields, - ]); - - async function loadMetricFieldStats() { - // Only request data for fields that exist in documents. - if (metricConfigs.length === 0) { - return; - } - - const configsToLoad = metricConfigs.filter( - (config) => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - // Obtain the interval to use for date histogram aggregations - // (such as the document count chart). Aim for 75 bars. - const buckets = getTimeBuckets(); - - const tf = timefilter as any; - let earliest: number | undefined; - let latest: number | undefined; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - const bounds = tf.getActiveBounds(); - const BAR_TARGET = 75; - buckets.setInterval('auto'); - buckets.setBounds(bounds); - buckets.setBarTarget(BAR_TARGET); - const aggInterval = buckets.getInterval(); - - try { - const metricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existMetricFields, - aggInterval.asMilliseconds() - ); - - // Add the metric stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - metricConfigs.forEach((config) => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - configWithStats.loading = false; - configs.push(configWithStats); - } else { - // Document count card. - configWithStats.stats = metricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === undefined - ); - - if (configWithStats.stats !== undefined) { - // Add earliest / latest of timefilter for setting x axis domain. - configWithStats.stats.timeRangeEarliest = earliest; - configWithStats.stats.timeRangeLatest = latest; - } - setDocumentCountStats(configWithStats); - } - }); - - setMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - async function loadNonMetricFieldStats() { - // Only request data for fields that exist in documents. - if (nonMetricConfigs.length === 0) { - return; - } - - const configsToLoad = nonMetricConfigs.filter( - (config) => config.existsInDocs === true && config.loading === true - ); - if (configsToLoad.length === 0) { - return; - } - - // Pass the field name, type and cardinality in the request. - // Top values will be obtained on a sample if cardinality > 100000. - const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => { - const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 }; - if (config.stats !== undefined && config.stats.cardinality !== undefined) { - props.cardinality = config.stats.cardinality; - } - return props; - }); - - const tf = timefilter as any; - let earliest; - let latest; - if (currentIndexPattern.timeFieldName !== undefined) { - earliest = tf.getActiveBounds().min.valueOf(); - latest = tf.getActiveBounds().max.valueOf(); - } - - try { - const nonMetricFieldStats = await dataLoader.loadFieldStats( - searchQuery, - samplerShardSize, - earliest, - latest, - existNonMetricFields - ); - - // Add the field stats to the existing stats in the corresponding config. - const configs: FieldVisConfig[] = []; - nonMetricConfigs.forEach((config) => { - const configWithStats = { ...config }; - if (config.fieldName !== undefined) { - configWithStats.stats = { - ...configWithStats.stats, - ...nonMetricFieldStats.find( - (fieldStats: any) => fieldStats.fieldName === config.fieldName - ), - }; - } - configWithStats.loading = false; - configs.push(configWithStats); - }); - - setNonMetricConfigs(configs); - } catch (err) { - dataLoader.displayError(err); - } - } - - useEffect(() => { - loadOverallStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, samplerShardSize, lastRefresh]); + }, [currentIndexPattern, indexPatternFields, nonMetricsLoaded, overallStats, showEmptyFields]); useEffect(() => { createMetricCards(); @@ -500,27 +438,8 @@ export const useDataVisualizerGridData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [overallStats, showEmptyFields]); - useEffect(() => { - loadMetricFieldStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricConfigs]); - - useEffect(() => { - loadNonMetricFieldStats(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nonMetricConfigs]); - - useEffect(() => { - createMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [metricsLoaded]); - - useEffect(() => { - createNonMetricCards(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nonMetricsLoaded]); - const configs = useMemo(() => { + const fieldStats = strategyResponse.fieldStats; let combinedConfigs = [...nonMetricConfigs, ...metricConfigs]; if (visibleFieldTypes && visibleFieldTypes.length > 0) { combinedConfigs = combinedConfigs.filter( @@ -533,8 +452,27 @@ export const useDataVisualizerGridData = ( ); } + if (fieldStats) { + combinedConfigs = combinedConfigs.map((c) => { + const loadedFullStats = fieldStats.get(c.fieldName) ?? {}; + return loadedFullStats + ? { + ...c, + loading: false, + stats: { ...c.stats, ...loadedFullStats }, + } + : c; + }); + } + return combinedConfigs; - }, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]); + }, [ + nonMetricConfigs, + metricConfigs, + visibleFieldTypes, + visibleFieldNames, + strategyResponse.fieldStats, + ]); // Some actions open up fly-out or popup // This variable is used to keep track of them and clean up when unmounting @@ -575,13 +513,16 @@ export const useDataVisualizerGridData = ( }, [input.indexPattern, services, searchQueryLanguage, searchString]); return { + progress: combinedProgress, configs, searchQueryLanguage, searchString, searchQuery, extendedColumns, - documentCountStats, + documentCountStats: overallStats.documentCountStats, metricsStats, - loaded: metricsLoaded && nonMetricsLoaded, + overallStats, + timefilter, + setLastRefresh, }; }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts new file mode 100644 index 0000000000000..64654d56db05b --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_field_stats.ts @@ -0,0 +1,287 @@ +/* + * 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 { useCallback, useEffect, useReducer, useRef, useState } from 'react'; +import { combineLatest, Observable, Subject, Subscription } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { last, cloneDeep } from 'lodash'; +import { switchMap } from 'rxjs/operators'; +import type { + DataStatsFetchProgress, + FieldStatsSearchStrategyReturnBase, + OverallStatsSearchStrategyParams, + FieldStatsCommonRequestParams, + Field, +} from '../../../../common/types/field_stats'; +import { useDataVisualizerKibana } from '../../kibana_context'; +import type { FieldRequestConfig } from '../../../../common'; +import type { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state'; +import { + buildBaseFilterCriteria, + getSafeAggregationName, +} from '../../../../common/utils/query_utils'; +import type { FieldStats, FieldStatsError } from '../../../../common/types/field_stats'; +import { getInitialProgress, getReducer } from '../progress_utils'; +import { MAX_EXAMPLES_DEFAULT } from '../search_strategy/requests/constants'; +import type { ISearchOptions } from '../../../../../../../src/plugins/data/common'; +import { getFieldsStats } from '../search_strategy/requests/get_fields_stats'; +interface FieldStatsParams { + metricConfigs: FieldRequestConfig[]; + nonMetricConfigs: FieldRequestConfig[]; +} + +const createBatchedRequests = (fields: Field[], maxBatchSize = 10) => { + // Batch up fields by type, getting stats for multiple fields at a time. + const batches: Field[][] = []; + const batchedFields: { [key: string]: Field[][] } = {}; + + fields.forEach((field) => { + const fieldType = field.type; + if (batchedFields[fieldType] === undefined) { + batchedFields[fieldType] = [[]]; + } + let lastArray: Field[] = last(batchedFields[fieldType]) as Field[]; + if (lastArray.length === maxBatchSize) { + lastArray = []; + batchedFields[fieldType].push(lastArray); + } + lastArray.push(field); + }); + + Object.values(batchedFields).forEach((lists) => { + batches.push(...lists); + }); + return batches; +}; + +export function useFieldStatsSearchStrategy( + searchStrategyParams: OverallStatsSearchStrategyParams | undefined, + fieldStatsParams: FieldStatsParams | undefined, + initialDataVisualizerListState: DataVisualizerIndexBasedAppState +): FieldStatsSearchStrategyReturnBase { + const { + services: { + data, + notifications: { toasts }, + }, + } = useDataVisualizerKibana(); + + const [fieldStats, setFieldStats] = useState>(); + const [fetchState, setFetchState] = useReducer( + getReducer(), + getInitialProgress() + ); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + const retries$ = useRef(); + + const startFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + retries$.current?.unsubscribe(); + + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + setFetchState({ + ...getInitialProgress(), + error: undefined, + }); + setFieldStats(undefined); + + if ( + !searchStrategyParams || + !fieldStatsParams || + (fieldStatsParams.metricConfigs.length === 0 && + fieldStatsParams.nonMetricConfigs.length === 0) + ) { + setFetchState({ + loaded: 100, + isRunning: false, + }); + + return; + } + + const { sortField, sortDirection } = initialDataVisualizerListState; + /** + * Sort the list of fields by the initial sort field and sort direction + * Then divide into chunks by the initial page size + */ + + let sortedConfigs = [...fieldStatsParams.metricConfigs, ...fieldStatsParams.nonMetricConfigs]; + + if (sortField === 'fieldName' || sortField === 'type') { + sortedConfigs = sortedConfigs.sort((a, b) => a[sortField].localeCompare(b[sortField])); + } + if (sortDirection === 'desc') { + sortedConfigs = sortedConfigs.reverse(); + } + + const filterCriteria = buildBaseFilterCriteria( + searchStrategyParams.timeFieldName, + searchStrategyParams.earliest, + searchStrategyParams.latest, + searchStrategyParams.searchQuery + ); + + const params: FieldStatsCommonRequestParams = { + index: searchStrategyParams.index, + samplerShardSize: searchStrategyParams.samplerShardSize, + timeFieldName: searchStrategyParams.timeFieldName, + earliestMs: searchStrategyParams.earliest, + latestMs: searchStrategyParams.latest, + runtimeFieldMap: searchStrategyParams.runtimeFieldMap, + intervalMs: searchStrategyParams.intervalMs, + query: { + bool: { + filter: filterCriteria, + }, + }, + maxExamples: MAX_EXAMPLES_DEFAULT, + }; + const searchOptions: ISearchOptions = { + abortSignal: abortCtrl.current.signal, + sessionId: searchStrategyParams?.sessionId, + }; + + const batches = createBatchedRequests( + sortedConfigs.map((config, idx) => ({ + fieldName: config.fieldName, + type: config.type, + cardinality: config.cardinality, + safeFieldName: getSafeAggregationName(config.fieldName, idx), + })), + 10 + ); + + const statsMap$ = new Subject(); + const fieldsToRetry$ = new Subject(); + + const fieldStatsSub = combineLatest( + batches + .map((batch) => getFieldsStats(data.search, params, batch, searchOptions)) + .filter((obs) => obs !== undefined) as Array> + ); + const onError = (error: any) => { + toasts.addError(error, { + title: i18n.translate('xpack.dataVisualizer.index.errorFetchingFieldStatisticsMessage', { + defaultMessage: 'Error fetching field statistics', + }), + }); + setFetchState({ + isRunning: false, + error, + }); + }; + + const onComplete = () => { + setFetchState({ + isRunning: false, + }); + }; + + // First, attempt to fetch field stats in batches of 10 + searchSubscription$.current = fieldStatsSub.subscribe({ + next: (resp) => { + if (resp) { + const statsMap = new Map(); + const failedFields: Field[] = []; + resp.forEach((batchResponse) => { + if (Array.isArray(batchResponse)) { + batchResponse.forEach((f) => { + if (f.fieldName !== undefined) { + statsMap.set(f.fieldName, f); + } + }); + } else { + // If an error occurred during batch + // retry each field in the failed batch individually + failedFields.push(...(batchResponse.fields ?? [])); + } + }); + + setFetchState({ + loaded: (statsMap.size / sortedConfigs.length) * 100, + isRunning: true, + }); + + setFieldStats(statsMap); + + if (failedFields.length > 0) { + statsMap$.next(statsMap); + fieldsToRetry$.next(failedFields); + } + } + }, + error: onError, + complete: onComplete, + }); + + // If any of batches failed, retry each of the failed field at least one time individually + retries$.current = combineLatest([ + statsMap$, + fieldsToRetry$.pipe( + switchMap((failedFields) => { + return combineLatest( + failedFields + .map((failedField) => + getFieldsStats(data.search, params, [failedField], searchOptions) + ) + .filter((obs) => obs !== undefined) + ); + }) + ), + ]).subscribe({ + next: (resp) => { + const statsMap = cloneDeep(resp[0]) as Map; + const fieldBatches = resp[1]; + + if (Array.isArray(fieldBatches)) { + fieldBatches.forEach((f) => { + if (Array.isArray(f) && f.length === 1) { + statsMap.set(f[0].fieldName, f[0]); + } + }); + setFieldStats(statsMap); + setFetchState({ + loaded: (statsMap.size / sortedConfigs.length) * 100, + isRunning: true, + }); + } + }, + error: onError, + complete: onComplete, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data.search, toasts, fieldStatsParams, initialDataVisualizerListState]); + + const cancelFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + + retries$.current?.unsubscribe(); + retries$.current = undefined; + + abortCtrl.current.abort(); + setFetchState({ + isRunning: false, + }); + }, []); + + // auto-update + useEffect(() => { + startFetch(); + return cancelFetch; + }, [startFetch, cancelFetch]); + + return { + progress: fetchState, + fieldStats, + startFetch, + cancelFetch, + }; +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts new file mode 100644 index 0000000000000..92a95bfacea42 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts @@ -0,0 +1,267 @@ +/* + * 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 { useCallback, useEffect, useState, useRef, useMemo, useReducer } from 'react'; +import { forkJoin, of, Subscription } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; +import type { ToastsStart } from 'kibana/public'; +import { chunk } from 'lodash'; +import { useDataVisualizerKibana } from '../../kibana_context'; +import { + AggregatableFieldOverallStats, + checkAggregatableFieldsExistRequest, + checkNonAggregatableFieldExistsRequest, + processAggregatableFieldsExistResponse, + processNonAggregatableFieldsExistResponse, +} from '../search_strategy/requests/overall_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, +} from '../../../../../../../src/plugins/data/common'; +import type { OverallStats } from '../types/overall_stats'; +import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view'; +import { extractErrorProperties } from '../utils/error_utils'; +import type { + DataStatsFetchProgress, + OverallStatsSearchStrategyParams, +} from '../../../../common/types/field_stats'; +import { + getDocumentCountStatsRequest, + processDocumentCountStats, +} from '../search_strategy/requests/get_document_stats'; +import { getInitialProgress, getReducer } from '../progress_utils'; + +function displayError(toastNotifications: ToastsStart, indexPattern: string, err: any) { + if (err.statusCode === 500) { + toastNotifications.addError(err, { + title: i18n.translate('xpack.dataVisualizer.index.dataLoader.internalServerErrorMessage', { + defaultMessage: + 'Error loading data in index {index}. {message}. ' + + 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', + values: { + index: indexPattern, + message: err.error ?? err.message, + }, + }), + }); + } else { + toastNotifications.addError(err, { + title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage', { + defaultMessage: 'Error loading data in index {index}. {message}.', + values: { + index: indexPattern, + message: err.error ?? err.message, + }, + }), + }); + } +} + +export function useOverallStats( + searchStrategyParams: TParams | undefined, + lastRefresh: number +): { + progress: DataStatsFetchProgress; + overallStats: OverallStats; +} { + const { + services: { + data, + notifications: { toasts }, + }, + } = useDataVisualizerKibana(); + + const [stats, setOverallStats] = useState(getDefaultPageState().overallStats); + const [fetchState, setFetchState] = useReducer( + getReducer(), + getInitialProgress() + ); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + + const startFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + if (!searchStrategyParams || lastRefresh === 0) return; + + setFetchState({ + ...getInitialProgress(), + error: undefined, + }); + + const { + aggregatableFields, + nonAggregatableFields, + index, + searchQuery, + timeFieldName, + earliest, + latest, + intervalMs, + runtimeFieldMap, + samplerShardSize, + } = searchStrategyParams; + + const searchOptions: ISearchOptions = { + abortSignal: abortCtrl.current.signal, + sessionId: searchStrategyParams?.sessionId, + }; + const nonAggregatableOverallStats$ = + nonAggregatableFields.length > 0 + ? forkJoin( + nonAggregatableFields.map((fieldName: string) => + data.search + .search( + { + params: checkNonAggregatableFieldExistsRequest( + index, + searchQuery, + fieldName, + timeFieldName, + earliest, + latest, + runtimeFieldMap + ), + }, + searchOptions + ) + .pipe( + switchMap((resp) => { + return of({ + ...resp, + rawResponse: { ...resp.rawResponse, fieldName }, + } as IKibanaSearchResponse); + }) + ) + ) + ) + : of(undefined); + + // Have to divide into smaller requests to avoid 413 payload too large + const aggregatableFieldsChunks = chunk(aggregatableFields, 30); + + const aggregatableOverallStats$ = forkJoin( + aggregatableFields.length > 0 + ? aggregatableFieldsChunks.map((aggregatableFieldsChunk) => + data.search + .search( + { + params: checkAggregatableFieldsExistRequest( + index, + searchQuery, + aggregatableFieldsChunk, + samplerShardSize, + timeFieldName, + earliest, + latest, + undefined, + runtimeFieldMap + ), + }, + searchOptions + ) + .pipe( + switchMap((resp) => { + return of({ + ...resp, + aggregatableFields: aggregatableFieldsChunk, + } as AggregatableFieldOverallStats); + }) + ) + ) + : of(undefined) + ); + + const documentCountStats$ = + timeFieldName !== undefined && intervalMs !== undefined && intervalMs > 0 + ? data.search.search( + { + params: getDocumentCountStatsRequest(searchStrategyParams), + }, + searchOptions + ) + : of(undefined); + const sub = forkJoin({ + documentCountStatsResp: documentCountStats$, + nonAggregatableOverallStatsResp: nonAggregatableOverallStats$, + aggregatableOverallStatsResp: aggregatableOverallStats$, + }).pipe( + switchMap( + ({ + documentCountStatsResp, + nonAggregatableOverallStatsResp, + aggregatableOverallStatsResp, + }) => { + const aggregatableOverallStats = processAggregatableFieldsExistResponse( + aggregatableOverallStatsResp, + aggregatableFields, + samplerShardSize + ); + const nonAggregatableOverallStats = processNonAggregatableFieldsExistResponse( + nonAggregatableOverallStatsResp, + nonAggregatableFields + ); + + return of({ + documentCountStats: processDocumentCountStats( + documentCountStatsResp?.rawResponse, + searchStrategyParams + ), + ...nonAggregatableOverallStats, + ...aggregatableOverallStats, + }); + } + ) + ); + + searchSubscription$.current = sub.subscribe({ + next: (overallStats) => { + if (overallStats) { + setOverallStats(overallStats); + } + }, + error: (error) => { + displayError(toasts, searchStrategyParams.index, extractErrorProperties(error)); + setFetchState({ + isRunning: false, + error, + }); + }, + complete: () => { + setFetchState({ + loaded: 100, + isRunning: false, + }); + }, + }); + }, [data.search, searchStrategyParams, toasts, lastRefresh]); + + const cancelFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + }, []); + + // auto-update + useEffect(() => { + startFetch(); + return cancelFetch; + }, [startFetch, cancelFetch]); + + return useMemo( + () => ({ + progress: fetchState, + overallStats: stats, + }), + [stats, fetchState] + ); +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/progress_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/progress_utils.ts new file mode 100644 index 0000000000000..f329fe47e75b0 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/progress_utils.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 { DataStatsFetchProgress } from '../../../common/types/field_stats'; + +export const getInitialProgress = (): DataStatsFetchProgress => ({ + isRunning: false, + loaded: 0, + total: 100, +}); + +export const getReducer = + () => + (prev: T, update: Partial): T => ({ + ...prev, + ...update, + }); diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/constants.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/constants.ts similarity index 81% rename from x-pack/plugins/data_visualizer/server/models/data_visualizer/constants.ts rename to x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/constants.ts index 91bd394aee797..6da11fd850acc 100644 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/constants.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/constants.ts @@ -11,3 +11,7 @@ export const AGGREGATABLE_EXISTS_REQUEST_BATCH_SIZE = 200; export const FIELDS_REQUEST_BATCH_SIZE = 10; export const MAX_CHART_COLUMNS = 20; + +export const MAX_EXAMPLES_DEFAULT = 10; +export const MAX_PERCENT = 100; +export const PERCENTILE_SPACING = 5; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts new file mode 100644 index 0000000000000..b5359915ef63e --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { get } from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { + Field, + BooleanFieldStats, + Aggs, + FieldStatsCommonRequestParams, +} from '../../../../../common/types/field_stats'; +import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + ISearchStart, +} from '../../../../../../../../src/plugins/data/public'; +import { extractErrorProperties } from '../../utils/error_utils'; + +export const getBooleanFieldsStatsRequest = ( + params: FieldStatsCommonRequestParams, + fields: Field[] +) => { + const { index, query, runtimeFieldMap, samplerShardSize } = params; + + const size = 0; + const aggs: Aggs = {}; + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + aggs[`${safeFieldName}_value_count`] = { + filter: { exists: { field: field.fieldName } }, + }; + aggs[`${safeFieldName}_values`] = { + terms: { + field: field.fieldName, + size: 2, + }, + }; + }); + const searchBody = { + query, + aggs: buildSamplerAggregation(aggs, samplerShardSize), + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchBooleanFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +): Observable => { + const { samplerShardSize } = params; + const request: estypes.SearchRequest = getBooleanFieldsStatsRequest(params, fields); + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => + of({ + fields, + error: extractErrorProperties(e), + } as FieldStatsError) + ), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + + const aggregations = resp.rawResponse.aggregations; + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + + const batchStats: BooleanFieldStats[] = fields.map((field, i) => { + const safeFieldName = field.fieldName; + const stats: BooleanFieldStats = { + fieldName: field.fieldName, + count: get(aggregations, [...aggsPath, `${safeFieldName}_value_count`, 'doc_count'], 0), + trueCount: 0, + falseCount: 0, + }; + + const valueBuckets: Array<{ [key: string]: number }> = get( + aggregations, + [...aggsPath, `${safeFieldName}_values`, 'buckets'], + [] + ); + valueBuckets.forEach((bucket) => { + stats[`${bucket.key_as_string}Count`] = bucket.doc_count; + }); + return stats; + }); + + return batchStats; + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts new file mode 100644 index 0000000000000..07bdc8c14301c --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts @@ -0,0 +1,106 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get } from 'lodash'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; +import type { Field, DateFieldStats, Aggs } from '../../../../../common/types/field_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + ISearchStart, +} from '../../../../../../../../src/plugins/data/public'; +import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; +import { extractErrorProperties } from '../../utils/error_utils'; + +export const getDateFieldsStatsRequest = ( + params: FieldStatsCommonRequestParams, + fields: Field[] +) => { + const { index, query, runtimeFieldMap, samplerShardSize } = params; + + const size = 0; + + const aggs: Aggs = {}; + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + aggs[`${safeFieldName}_field_stats`] = { + filter: { exists: { field: field.fieldName } }, + aggs: { + actual_stats: { + stats: { field: field.fieldName }, + }, + }, + }; + }); + + const searchBody = { + query, + aggs: buildSamplerAggregation(aggs, samplerShardSize), + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchDateFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +): Observable => { + const { samplerShardSize } = params; + + const request: estypes.SearchRequest = getDateFieldsStatsRequest(params, fields); + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => + of({ + fields, + error: extractErrorProperties(e), + } as FieldStatsError) + ), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + const aggregations = resp.rawResponse.aggregations; + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + + const batchStats: DateFieldStats[] = fields.map((field, i) => { + const safeFieldName = field.safeFieldName; + const docCount = get( + aggregations, + [...aggsPath, `${safeFieldName}_field_stats`, 'doc_count'], + 0 + ); + const fieldStatsResp = get( + aggregations, + [...aggsPath, `${safeFieldName}_field_stats`, 'actual_stats'], + {} + ); + return { + fieldName: field.fieldName, + count: docCount, + earliest: get(fieldStatsResp, 'min', 0), + latest: get(fieldStatsResp, 'max', 0), + } as DateFieldStats; + }); + return batchStats; + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts new file mode 100644 index 0000000000000..cdd69f5d3a369 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { each, get } from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { buildBaseFilterCriteria } from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { + DocumentCountStats, + OverallStatsSearchStrategyParams, +} from '../../../../../common/types/field_stats'; + +export const getDocumentCountStatsRequest = (params: OverallStatsSearchStrategyParams) => { + const { + index, + timeFieldName, + earliest: earliestMs, + latest: latestMs, + runtimeFieldMap, + searchQuery, + intervalMs, + } = params; + + const size = 0; + const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, searchQuery); + + // Don't use the sampler aggregation as this can lead to some potentially + // confusing date histogram results depending on the date range of data amongst shards. + + const aggs = { + eventRate: { + date_histogram: { + field: timeFieldName, + fixed_interval: `${intervalMs}ms`, + min_doc_count: 1, + }, + }, + }; + + const searchBody = { + query: { + bool: { + filter: filterCriteria, + }, + }, + aggs, + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + return { + index, + size, + body: searchBody, + }; +}; + +export const processDocumentCountStats = ( + body: estypes.SearchResponse | undefined, + params: OverallStatsSearchStrategyParams +): DocumentCountStats | undefined => { + if ( + !body || + params.intervalMs === undefined || + params.earliest === undefined || + params.latest === undefined + ) { + return undefined; + } + const buckets: { [key: string]: number } = {}; + const dataByTimeBucket: Array<{ key: string; doc_count: number }> = get( + body, + ['aggregations', 'eventRate', 'buckets'], + [] + ); + each(dataByTimeBucket, (dataForTime) => { + const time = dataForTime.key; + buckets[time] = dataForTime.doc_count; + }); + + return { + interval: params.intervalMs, + buckets, + timeRangeEarliest: params.earliest, + timeRangeLatest: params.latest, + }; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts new file mode 100644 index 0000000000000..618e47bb97e1d --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get } from 'lodash'; +import { combineLatest, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { buildBaseFilterCriteria } from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { + Field, + FieldExamples, + FieldStatsCommonRequestParams, +} from '../../../../../common/types/field_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + ISearchStart, +} from '../../../../../../../../src/plugins/data/public'; +import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; +import { extractErrorProperties } from '../../utils/error_utils'; +import { MAX_EXAMPLES_DEFAULT } from './constants'; + +export const getFieldExamplesRequest = (params: FieldStatsCommonRequestParams, field: Field) => { + const { index, timeFieldName, earliestMs, latestMs, query, runtimeFieldMap, maxExamples } = + params; + + // Request at least 100 docs so that we have a chance of obtaining + // 'maxExamples' of the field. + const size = Math.max(100, maxExamples ?? MAX_EXAMPLES_DEFAULT); + const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); + + // Use an exists filter to return examples of the field. + if (Array.isArray(filterCriteria)) { + filterCriteria.push({ + exists: { field: field.fieldName }, + }); + } + + const searchBody = { + fields: [field.fieldName], + _source: false, + query: { + bool: { + filter: filterCriteria, + }, + }, + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchFieldsExamples = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +) => { + const { maxExamples } = params; + return combineLatest( + fields.map((field) => { + const request: estypes.SearchRequest = getFieldExamplesRequest(params, field); + + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => + of({ + fieldName: field.fieldName, + fields, + error: extractErrorProperties(e), + } as FieldStatsError) + ), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + const body = resp.rawResponse; + const stats = { + fieldName: field.fieldName, + examples: [] as unknown[], + } as FieldExamples; + + if (body.hits.total > 0) { + const hits = body.hits.hits; + for (let i = 0; i < hits.length; i++) { + // Use lodash get() to support field names containing dots. + const doc: object[] | undefined = get(hits[i].fields, field.fieldName); + // the results from fields query is always an array + if (Array.isArray(doc) && doc.length > 0) { + const example = doc[0]; + if (example !== undefined && stats.examples.indexOf(example) === -1) { + stats.examples.push(example); + if (stats.examples.length === maxExamples) { + break; + } + } + } + } + } + + return stats; + }) + ); + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts new file mode 100644 index 0000000000000..aa19aa9fbb495 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_fields_stats.ts @@ -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 type { Observable } from 'rxjs'; +import type { FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; +import type { FieldStatsError } from '../../../../../common/types/field_stats'; +import type { ISearchOptions } from '../../../../../../../../src/plugins/data/common'; +import { ISearchStart } from '../../../../../../../../src/plugins/data/public'; +import type { FieldStats } from '../../../../../common/types/field_stats'; +import { JOB_FIELD_TYPES } from '../../../../../common'; +import { fetchDateFieldsStats } from './get_date_field_stats'; +import { fetchBooleanFieldsStats } from './get_boolean_field_stats'; +import { fetchFieldsExamples } from './get_field_examples'; +import { fetchNumericFieldsStats } from './get_numeric_field_stats'; +import { fetchStringFieldsStats } from './get_string_field_stats'; + +export const getFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Array<{ + fieldName: string; + type: string; + cardinality: number; + safeFieldName: string; + }>, + options: ISearchOptions +): Observable | undefined => { + const fieldType = fields[0].type; + switch (fieldType) { + case JOB_FIELD_TYPES.NUMBER: + return fetchNumericFieldsStats(dataSearch, params, fields, options); + case JOB_FIELD_TYPES.KEYWORD: + case JOB_FIELD_TYPES.IP: + return fetchStringFieldsStats(dataSearch, params, fields, options); + case JOB_FIELD_TYPES.DATE: + return fetchDateFieldsStats(dataSearch, params, fields, options); + case JOB_FIELD_TYPES.BOOLEAN: + return fetchBooleanFieldsStats(dataSearch, params, fields, options); + case JOB_FIELD_TYPES.TEXT: + return fetchFieldsExamples(dataSearch, params, fields, options); + default: + // Use an exists filter on the the field name to get + // examples of the field, so cannot batch up. + return fetchFieldsExamples(dataSearch, params, fields, options); + } +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts new file mode 100644 index 0000000000000..89ae7598b30fd --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts @@ -0,0 +1,207 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { find, get } from 'lodash'; +import { catchError, map } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { AggregationsTermsAggregation } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + MAX_PERCENT, + PERCENTILE_SPACING, + SAMPLER_TOP_TERMS_SHARD_SIZE, + SAMPLER_TOP_TERMS_THRESHOLD, +} from './constants'; +import { + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { Aggs, FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; +import type { + Field, + NumericFieldStats, + Bucket, + FieldStatsError, +} from '../../../../../common/types/field_stats'; +import { processDistributionData } from '../../utils/process_distribution_data'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, +} from '../../../../../../../../src/plugins/data/common'; +import type { ISearchStart } from '../../../../../../../../src/plugins/data/public'; +import { extractErrorProperties } from '../../utils/error_utils'; +import { isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; + +export const getNumericFieldsStatsRequest = ( + params: FieldStatsCommonRequestParams, + fields: Field[] +) => { + const { index, query, runtimeFieldMap, samplerShardSize } = params; + + const size = 0; + + // Build the percents parameter which defines the percentiles to query + // for the metric distribution data. + // Use a fixed percentile spacing of 5%. + let count = 0; + const percents = Array.from( + Array(MAX_PERCENT / PERCENTILE_SPACING), + () => (count += PERCENTILE_SPACING) + ); + + const aggs: Aggs = {}; + + fields.forEach((field, i) => { + const { safeFieldName } = field; + + aggs[`${safeFieldName}_field_stats`] = { + filter: { exists: { field: field.fieldName } }, + aggs: { + actual_stats: { + stats: { field: field.fieldName }, + }, + }, + }; + aggs[`${safeFieldName}_percentiles`] = { + percentiles: { + field: field.fieldName, + percents, + keyed: false, + }, + }; + + const top = { + terms: { + field: field.fieldName, + size: 10, + order: { + _count: 'desc', + }, + } as AggregationsTermsAggregation, + }; + + // If cardinality >= SAMPLE_TOP_TERMS_THRESHOLD, run the top terms aggregation + // in a sampler aggregation, even if no sampling has been specified (samplerShardSize < 1). + if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { + aggs[`${safeFieldName}_top`] = { + sampler: { + shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE, + }, + aggs: { + top, + }, + }; + } else { + aggs[`${safeFieldName}_top`] = top; + } + }); + + const searchBody = { + query, + aggs: buildSamplerAggregation(aggs, samplerShardSize), + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchNumericFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +): Observable => { + const { samplerShardSize } = params; + const request: estypes.SearchRequest = getNumericFieldsStatsRequest(params, fields); + + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => { + // @todo: kick off another requests individually + return of({ + fields, + error: extractErrorProperties(e), + } as FieldStatsError); + }), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + + const aggregations = resp.rawResponse.aggregations; + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + + const batchStats: NumericFieldStats[] = []; + + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + const docCount = get( + aggregations, + [...aggsPath, `${safeFieldName}_field_stats`, 'doc_count'], + 0 + ); + const fieldStatsResp = get( + aggregations, + [...aggsPath, `${safeFieldName}_field_stats`, 'actual_stats'], + {} + ); + + const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; + if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { + topAggsPath.push('top'); + } + + const topValues: Bucket[] = get(aggregations, [...topAggsPath, 'buckets'], []); + + const stats: NumericFieldStats = { + fieldName: field.fieldName, + count: docCount, + min: get(fieldStatsResp, 'min', 0), + max: get(fieldStatsResp, 'max', 0), + avg: get(fieldStatsResp, 'avg', 0), + isTopValuesSampled: + field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD || samplerShardSize > 0, + topValues, + topValuesSampleSize: topValues.reduce( + (acc, curr) => acc + curr.doc_count, + get(aggregations, [...topAggsPath, 'sum_other_doc_count'], 0) + ), + topValuesSamplerShardSize: + field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD + ? SAMPLER_TOP_TERMS_SHARD_SIZE + : samplerShardSize, + }; + + if (stats.count > 0) { + const percentiles = get( + aggregations, + [...aggsPath, `${safeFieldName}_percentiles`, 'values'], + [] + ); + const medianPercentile: { value: number; key: number } | undefined = find(percentiles, { + key: 50, + }); + stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; + stats.distribution = processDistributionData( + percentiles, + PERCENTILE_SPACING, + stats.min + ); + } + + batchStats.push(stats); + }); + + return batchStats; + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts new file mode 100644 index 0000000000000..024464c1947c8 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get } from 'lodash'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { AggregationsTermsAggregation } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SAMPLER_TOP_TERMS_SHARD_SIZE, SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; +import { + buildSamplerAggregation, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import type { + Aggs, + Bucket, + Field, + FieldStatsCommonRequestParams, + StringFieldStats, +} from '../../../../../common/types/field_stats'; +import type { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + ISearchStart, +} from '../../../../../../../../src/plugins/data/public'; +import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; +import { extractErrorProperties } from '../../utils/error_utils'; + +export const getStringFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + fields: Field[] +) => { + const { index, query, runtimeFieldMap, samplerShardSize } = params; + + const size = 0; + + const aggs: Aggs = {}; + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + const top = { + terms: { + field: field.fieldName, + size: 10, + order: { + _count: 'desc', + }, + } as AggregationsTermsAggregation, + }; + + // If cardinality >= SAMPLE_TOP_TERMS_THRESHOLD, run the top terms aggregation + // in a sampler aggregation, even if no sampling has been specified (samplerShardSize < 1). + if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { + aggs[`${safeFieldName}_top`] = { + sampler: { + shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE, + }, + aggs: { + top, + }, + }; + } else { + aggs[`${safeFieldName}_top`] = top; + } + }); + + const searchBody = { + query, + aggs: buildSamplerAggregation(aggs, samplerShardSize), + ...(isPopulatedObject(runtimeFieldMap) ? { runtime_mappings: runtimeFieldMap } : {}), + }; + + return { + index, + size, + body: searchBody, + }; +}; + +export const fetchStringFieldsStats = ( + dataSearch: ISearchStart, + params: FieldStatsCommonRequestParams, + fields: Field[], + options: ISearchOptions +): Observable => { + const { samplerShardSize } = params; + const request: estypes.SearchRequest = getStringFieldStatsRequest(params, fields); + + return dataSearch + .search({ params: request }, options) + .pipe( + catchError((e) => + of({ + fields, + error: extractErrorProperties(e), + } as FieldStatsError) + ), + map((resp) => { + if (!isIKibanaSearchResponse(resp)) return resp; + const aggregations = resp.rawResponse.aggregations; + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + const batchStats: StringFieldStats[] = []; + + fields.forEach((field, i) => { + const safeFieldName = field.safeFieldName; + + const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; + if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { + topAggsPath.push('top'); + } + + const topValues: Bucket[] = get(aggregations, [...topAggsPath, 'buckets'], []); + + const stats = { + fieldName: field.fieldName, + isTopValuesSampled: + field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD || samplerShardSize > 0, + topValues, + topValuesSampleSize: topValues.reduce( + (acc, curr) => acc + curr.doc_count, + get(aggregations, [...topAggsPath, 'sum_other_doc_count'], 0) + ), + topValuesSamplerShardSize: + field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD + ? SAMPLER_TOP_TERMS_SHARD_SIZE + : samplerShardSize, + }; + + batchStats.push(stats); + }); + + return batchStats; + }) + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts new file mode 100644 index 0000000000000..fb392cc17b05b --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts @@ -0,0 +1,227 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { get } from 'lodash'; +import { Query } from '@kbn/es-query'; +import { + buildBaseFilterCriteria, + buildSamplerAggregation, + getSafeAggregationName, + getSamplerAggregationsResponsePath, +} from '../../../../../common/utils/query_utils'; +import { getDatafeedAggregations } from '../../../../../common/utils/datafeed_utils'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import { IKibanaSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { AggregatableField, NonAggregatableField } from '../../types/overall_stats'; +import { AggCardinality, Aggs } from '../../../../../common/types/field_stats'; + +export const checkAggregatableFieldsExistRequest = ( + indexPatternTitle: string, + query: Query['query'], + aggregatableFields: string[], + samplerShardSize: number, + timeFieldName: string | undefined, + earliestMs?: number, + latestMs?: number, + datafeedConfig?: estypes.MlDatafeed, + runtimeMappings?: estypes.MappingRuntimeFields +): estypes.SearchRequest => { + const index = indexPatternTitle; + const size = 0; + const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); + const datafeedAggregations = getDatafeedAggregations(datafeedConfig); + + // Value count aggregation faster way of checking if field exists than using + // filter aggregation with exists query. + const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {}; + + // Combine runtime fields from the data view as well as the datafeed + const combinedRuntimeMappings: estypes.MappingRuntimeFields = { + ...(isPopulatedObject(runtimeMappings) ? runtimeMappings : {}), + ...(isPopulatedObject(datafeedConfig) && isPopulatedObject(datafeedConfig.runtime_mappings) + ? datafeedConfig.runtime_mappings + : {}), + }; + + aggregatableFields.forEach((field, i) => { + const safeFieldName = getSafeAggregationName(field, i); + aggs[`${safeFieldName}_count`] = { + filter: { exists: { field } }, + }; + + let cardinalityField: AggCardinality; + if (datafeedConfig?.script_fields?.hasOwnProperty(field)) { + cardinalityField = aggs[`${safeFieldName}_cardinality`] = { + cardinality: { script: datafeedConfig?.script_fields[field].script }, + }; + } else { + cardinalityField = { + cardinality: { field }, + }; + } + aggs[`${safeFieldName}_cardinality`] = cardinalityField; + }); + + const searchBody = { + query: { + bool: { + filter: filterCriteria, + }, + }, + ...(isPopulatedObject(aggs) ? { aggs: buildSamplerAggregation(aggs, samplerShardSize) } : {}), + ...(isPopulatedObject(combinedRuntimeMappings) + ? { runtime_mappings: combinedRuntimeMappings } + : {}), + }; + + return { + index, + track_total_hits: true, + size, + body: searchBody, + }; +}; + +export interface AggregatableFieldOverallStats extends IKibanaSearchResponse { + aggregatableFields: string[]; +} +export const processAggregatableFieldsExistResponse = ( + responses: AggregatableFieldOverallStats[] | undefined, + aggregatableFields: string[], + samplerShardSize: number, + datafeedConfig?: estypes.MlDatafeed +) => { + const stats = { + totalCount: 0, + aggregatableExistsFields: [] as AggregatableField[], + aggregatableNotExistsFields: [] as AggregatableField[], + }; + + if (!responses || aggregatableFields.length === 0) return stats; + + responses.forEach(({ rawResponse: body, aggregatableFields: aggregatableFieldsChunk }) => { + const aggregations = body.aggregations; + const totalCount = (body.hits.total as estypes.SearchTotalHits).value ?? body.hits.total; + stats.totalCount = totalCount as number; + + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + const sampleCount = + samplerShardSize > 0 ? get(aggregations, ['sample', 'doc_count'], 0) : totalCount; + aggregatableFieldsChunk.forEach((field, i) => { + const safeFieldName = getSafeAggregationName(field, i); + const count = get(aggregations, [...aggsPath, `${safeFieldName}_count`, 'doc_count'], 0); + if (count > 0) { + const cardinality = get( + aggregations, + [...aggsPath, `${safeFieldName}_cardinality`, 'value'], + 0 + ); + stats.aggregatableExistsFields.push({ + fieldName: field, + existsInDocs: true, + stats: { + sampleCount, + count, + cardinality, + }, + }); + } else { + if ( + datafeedConfig?.script_fields?.hasOwnProperty(field) || + datafeedConfig?.runtime_mappings?.hasOwnProperty(field) + ) { + const cardinality = get( + aggregations, + [...aggsPath, `${safeFieldName}_cardinality`, 'value'], + 0 + ); + stats.aggregatableExistsFields.push({ + fieldName: field, + existsInDocs: true, + stats: { + sampleCount, + count, + cardinality, + }, + }); + } else { + stats.aggregatableNotExistsFields.push({ + fieldName: field, + existsInDocs: false, + stats: {}, + }); + } + } + }); + }); + + return stats as { + totalCount: number; + aggregatableExistsFields: AggregatableField[]; + aggregatableNotExistsFields: AggregatableField[]; + }; +}; + +export const checkNonAggregatableFieldExistsRequest = ( + indexPatternTitle: string, + query: Query['query'], + field: string, + timeFieldName: string | undefined, + earliestMs: number | undefined, + latestMs: number | undefined, + runtimeMappings?: estypes.MappingRuntimeFields +): estypes.SearchRequest => { + const index = indexPatternTitle; + const size = 0; + const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); + + const searchBody = { + query: { + bool: { + filter: filterCriteria, + }, + }, + ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), + }; + if (Array.isArray(filterCriteria)) { + filterCriteria.push({ exists: { field } }); + } + + return { + index, + size, + body: searchBody, + }; +}; + +export const processNonAggregatableFieldsExistResponse = ( + results: IKibanaSearchResponse[] | undefined, + nonAggregatableFields: string[] +) => { + const stats = { + nonAggregatableExistsFields: [] as NonAggregatableField[], + nonAggregatableNotExistsFields: [] as NonAggregatableField[], + }; + + if (!results || nonAggregatableFields.length === 0) return stats; + + nonAggregatableFields.forEach((fieldName) => { + const foundField = results.find((r) => r.rawResponse.fieldName === fieldName); + const existsInDocs = foundField !== undefined && foundField.rawResponse.hits.total > 0; + const fieldData: NonAggregatableField = { + fieldName, + existsInDocs, + }; + if (existsInDocs === true) { + stats.nonAggregatableExistsFields.push(fieldData); + } else { + stats.nonAggregatableNotExistsFields.push(fieldData); + } + }); + return stats; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/visualizer_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/visualizer_stats.ts deleted file mode 100644 index 3653936f3d12e..0000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/visualizer_stats.ts +++ /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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { lazyLoadModules } from '../../../lazy_load_bundle'; -import type { DocumentCounts, FieldRequestConfig, FieldVisStats } from '../../../../common/types'; -import { OverallStats } from '../types/overall_stats'; - -export function basePath() { - return '/internal/data_visualizer'; -} - -export async function getVisualizerOverallStats({ - indexPatternTitle, - query, - timeFieldName, - earliest, - latest, - samplerShardSize, - aggregatableFields, - nonAggregatableFields, - runtimeMappings, -}: { - indexPatternTitle: string; - query: any; - timeFieldName?: string; - earliest?: number; - latest?: number; - samplerShardSize?: number; - aggregatableFields: string[]; - nonAggregatableFields: string[]; - runtimeMappings?: estypes.MappingRuntimeFields; -}) { - const body = JSON.stringify({ - query, - timeFieldName, - earliest, - latest, - samplerShardSize, - aggregatableFields, - nonAggregatableFields, - runtimeMappings, - }); - - const fileUploadModules = await lazyLoadModules(); - return await fileUploadModules.getHttp().fetch({ - path: `${basePath()}/get_overall_stats/${indexPatternTitle}`, - method: 'POST', - body, - }); -} - -export async function getVisualizerFieldStats({ - indexPatternTitle, - query, - timeFieldName, - earliest, - latest, - samplerShardSize, - interval, - fields, - maxExamples, - runtimeMappings, -}: { - indexPatternTitle: string; - query: any; - timeFieldName?: string; - earliest?: number; - latest?: number; - samplerShardSize?: number; - interval?: number; - fields?: FieldRequestConfig[]; - maxExamples?: number; - runtimeMappings?: estypes.MappingRuntimeFields; -}) { - const body = JSON.stringify({ - query, - timeFieldName, - earliest, - latest, - samplerShardSize, - interval, - fields, - maxExamples, - runtimeMappings, - }); - - const fileUploadModules = await lazyLoadModules(); - return await fileUploadModules.getHttp().fetch<[DocumentCounts, FieldVisStats]>({ - path: `${basePath()}/get_field_stats/${indexPatternTitle}`, - method: 'POST', - body, - }); -} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/combined_query.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/combined_query.ts index 734a47d7f01b0..13590505a5d1a 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/combined_query.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/combined_query.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { Query } from '@kbn/es-query'; + export const SEARCH_QUERY_LANGUAGE = { KUERY: 'kuery', LUCENE: 'lucene', @@ -13,7 +15,7 @@ export const SEARCH_QUERY_LANGUAGE = { export type SearchQueryLanguage = typeof SEARCH_QUERY_LANGUAGE[keyof typeof SEARCH_QUERY_LANGUAGE]; export interface CombinedQuery { - searchString: string | { [key: string]: any }; + searchString: Query['query']; searchQueryLanguage: string; } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/overall_stats.ts index 2672dc69ac29a..84a6142f012da 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/overall_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/types/overall_stats.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { DocumentCountStats } from '../../../../common/types/field_stats'; + export interface AggregatableField { fieldName: string; stats: { @@ -19,8 +21,9 @@ export type NonAggregatableField = Omit; export interface OverallStats { totalCount: number; + documentCountStats?: DocumentCountStats; aggregatableExistsFields: AggregatableField[]; - aggregatableNotExistsFields: NonAggregatableField[]; - nonAggregatableExistsFields: AggregatableField[]; + aggregatableNotExistsFields: AggregatableField[]; + nonAggregatableExistsFields: NonAggregatableField[]; nonAggregatableNotExistsFields: NonAggregatableField[]; } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts index 9bb36496a149e..58c4c820c28d7 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts @@ -85,7 +85,7 @@ export function isDVResponseError(error: any): error is DVResponseError { } export function isBoomError(error: any): error is Boom.Boom { - return error.isBoom === true; + return error?.isBoom === true; } export function isWrappedError(error: any): error is WrappedError { diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/process_distribution_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts similarity index 95% rename from x-pack/plugins/data_visualizer/server/models/data_visualizer/process_distribution_data.ts rename to x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts index 4e40c2baaf701..46719c06e2264 100644 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/process_distribution_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/process_distribution_data.ts @@ -6,7 +6,7 @@ */ import { last } from 'lodash'; -import { Distribution } from '../../types'; +import type { Distribution } from '../../../../common/types/field_stats'; export const processDistributionData = ( percentiles: Array<{ value: number }>, @@ -49,7 +49,7 @@ export const processDistributionData = ( // Add in 0-5 and 95-100% if they don't add more // than 25% to the value range at either end. - const lastValue: number = (last(percentileBuckets) as any).value; + const lastValue: number = (last(percentileBuckets) as { value: number }).value; const maxDiff = 0.25 * (lastValue - lowerBound); if (lowerBound - dataMin < maxDiff) { percentileBuckets.splice(0, 0, percentiles[0]); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts index ad3229676b31b..586f636a088e1 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.test.ts @@ -75,7 +75,7 @@ const kqlSavedSearch: SavedSearch = { title: 'farequote_filter_and_kuery', description: '', columns: ['_source'], - // @ts-expect-error We don't need the full object here + // @ts-expect-error kibanaSavedObjectMeta: { searchSourceJSON: '{"highlightAll":true,"version":true,"query":{"query":"responsetime > 49","language":"kuery"},"filter":[{"meta":{"index":"90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"airline","value":"ASA","params":{"query":"ASA","type":"phrase"}},"query":{"match":{"airline":{"query":"ASA","type":"phrase"}}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts index 1401b1038b8f2..5ebdbcff0b26e 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts @@ -15,6 +15,7 @@ import { Query, Filter, } from '@kbn/es-query'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types'; import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/common'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query'; @@ -43,7 +44,7 @@ export function getDefaultQuery() { export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject | SavedSearch) { const search = isSavedSearchSavedObject(savedSearch) ? savedSearch?.attributes?.kibanaSavedObjectMeta - : // @ts-expect-error kibanaSavedObjectMeta does exist + : // @ts-ignore savedSearch?.kibanaSavedObjectMeta; const parsed = @@ -76,7 +77,7 @@ export function createMergedEsQuery( indexPattern?: IndexPattern, uiSettings?: IUiSettingsClient ) { - let combinedQuery: any = getDefaultQuery(); + let combinedQuery: QueryDslQueryContainer = getDefaultQuery(); if (query && query.language === SEARCH_QUERY_LANGUAGE.KUERY) { const ast = fromKueryExpression(query.query); @@ -86,12 +87,12 @@ export function createMergedEsQuery( if (combinedQuery.bool !== undefined) { const filterQuery = buildQueryFromFilters(filters, indexPattern); - if (Array.isArray(combinedQuery.bool.filter) === false) { + if (!Array.isArray(combinedQuery.bool.filter)) { combinedQuery.bool.filter = combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter]; } - if (Array.isArray(combinedQuery.bool.must_not) === false) { + if (!Array.isArray(combinedQuery.bool.must_not)) { combinedQuery.bool.must_not = combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not]; } @@ -145,8 +146,20 @@ export function getEsQueryFromSavedSearch({ savedSearch.searchSource.getParent() !== undefined && userQuery ) { + // Flattened query from search source may contain a clause that narrows the time range + // which might interfere with global time pickers so we need to remove + const savedQuery = + cloneDeep(savedSearch.searchSource.getSearchRequestBody()?.query) ?? getDefaultQuery(); + const timeField = savedSearch.searchSource.getField('index')?.timeFieldName; + + if (Array.isArray(savedQuery.bool.filter) && timeField !== undefined) { + savedQuery.bool.filter = savedQuery.bool.filter.filter( + (c: QueryDslQueryContainer) => + !(c.hasOwnProperty('range') && c.range?.hasOwnProperty(timeField)) + ); + } return { - searchQuery: savedSearch.searchSource.getSearchRequestBody()?.query ?? getDefaultQuery(), + searchQuery: savedQuery, searchString: userQuery.query, queryLanguage: userQuery.language as SearchQueryLanguage, }; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/check_fields_exist.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/check_fields_exist.ts deleted file mode 100644 index 24b4deeecdddd..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/check_fields_exist.ts +++ /dev/null @@ -1,183 +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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { get } from 'lodash'; -import { IScopedClusterClient } from 'kibana/server'; -import { AggCardinality, Aggs, FieldData } from '../../types'; -import { - buildBaseFilterCriteria, - buildSamplerAggregation, - getSafeAggregationName, - getSamplerAggregationsResponsePath, -} from '../../../common/utils/query_utils'; -import { getDatafeedAggregations } from '../../../common/utils/datafeed_utils'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; - -export const checkAggregatableFieldsExist = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - aggregatableFields: string[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs?: number, - latestMs?: number, - datafeedConfig?: estypes.MlDatafeed, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - const datafeedAggregations = getDatafeedAggregations(datafeedConfig); - - // Value count aggregation faster way of checking if field exists than using - // filter aggregation with exists query. - const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {}; - - // Combine runtime fields from the data view as well as the datafeed - const combinedRuntimeMappings: estypes.MappingRuntimeFields = { - ...(isPopulatedObject(runtimeMappings) ? runtimeMappings : {}), - ...(isPopulatedObject(datafeedConfig) && isPopulatedObject(datafeedConfig.runtime_mappings) - ? datafeedConfig.runtime_mappings - : {}), - }; - - aggregatableFields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field, i); - aggs[`${safeFieldName}_count`] = { - filter: { exists: { field } }, - }; - - let cardinalityField: AggCardinality; - if (datafeedConfig?.script_fields?.hasOwnProperty(field)) { - cardinalityField = aggs[`${safeFieldName}_cardinality`] = { - cardinality: { script: datafeedConfig?.script_fields[field].script }, - }; - } else { - cardinalityField = { - cardinality: { field }, - }; - } - aggs[`${safeFieldName}_cardinality`] = cardinalityField; - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - ...(isPopulatedObject(aggs) ? { aggs: buildSamplerAggregation(aggs, samplerShardSize) } : {}), - ...(isPopulatedObject(combinedRuntimeMappings) - ? { runtime_mappings: combinedRuntimeMappings } - : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - track_total_hits: true, - size, - body: searchBody, - }); - - const aggregations = body.aggregations; - // @ts-expect-error incorrect search response type - const totalCount = body.hits.total.value; - const stats = { - totalCount, - aggregatableExistsFields: [] as FieldData[], - aggregatableNotExistsFields: [] as FieldData[], - }; - - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const sampleCount = - samplerShardSize > 0 ? get(aggregations, ['sample', 'doc_count'], 0) : totalCount; - aggregatableFields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field, i); - const count = get(aggregations, [...aggsPath, `${safeFieldName}_count`, 'doc_count'], 0); - if (count > 0) { - const cardinality = get( - aggregations, - [...aggsPath, `${safeFieldName}_cardinality`, 'value'], - 0 - ); - stats.aggregatableExistsFields.push({ - fieldName: field, - existsInDocs: true, - stats: { - sampleCount, - count, - cardinality, - }, - }); - } else { - if ( - datafeedConfig?.script_fields?.hasOwnProperty(field) || - datafeedConfig?.runtime_mappings?.hasOwnProperty(field) - ) { - const cardinality = get( - aggregations, - [...aggsPath, `${safeFieldName}_cardinality`, 'value'], - 0 - ); - stats.aggregatableExistsFields.push({ - fieldName: field, - existsInDocs: true, - stats: { - sampleCount, - count, - cardinality, - }, - }); - } else { - stats.aggregatableNotExistsFields.push({ - fieldName: field, - existsInDocs: false, - }); - } - } - }); - - return stats; -}; - -export const checkNonAggregatableFieldExists = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - field: string, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - filterCriteria.push({ exists: { field } }); - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - // @ts-expect-error incorrect search response type - return body.hits.total.value > 0; -}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts deleted file mode 100644 index 42e7f93cc8789..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts +++ /dev/null @@ -1,489 +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 { each, last } from 'lodash'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { JOB_FIELD_TYPES } from '../../../common'; -import type { - BatchStats, - FieldData, - HistogramField, - Field, - DocumentCountStats, - FieldExamples, -} from '../../types'; -import { getHistogramsForFields } from './get_histogram_for_fields'; -import { - checkAggregatableFieldsExist, - checkNonAggregatableFieldExists, -} from './check_fields_exist'; -import { AGGREGATABLE_EXISTS_REQUEST_BATCH_SIZE, FIELDS_REQUEST_BATCH_SIZE } from './constants'; -import { getFieldExamples } from './get_field_examples'; -import { - getBooleanFieldsStats, - getDateFieldsStats, - getDocumentCountStats, - getNumericFieldsStats, - getStringFieldsStats, -} from './get_fields_stats'; -import { wrapError } from '../../utils/error_wrapper'; - -export class DataVisualizer { - private _client: IScopedClusterClient; - - constructor(client: IScopedClusterClient) { - this._client = client; - } - - // Obtains overall stats on the fields in the supplied data view, returning an object - // containing the total document count, and four arrays showing which of the supplied - // aggregatable and non-aggregatable fields do or do not exist in documents. - // Sampling will be used if supplied samplerShardSize > 0. - async getOverallStats( - indexPatternTitle: string, - query: object, - aggregatableFields: string[], - nonAggregatableFields: string[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - const stats = { - totalCount: 0, - aggregatableExistsFields: [] as FieldData[], - aggregatableNotExistsFields: [] as FieldData[], - nonAggregatableExistsFields: [] as FieldData[], - nonAggregatableNotExistsFields: [] as FieldData[], - errors: [] as any[], - }; - - // To avoid checking for the existence of too many aggregatable fields in one request, - // split the check into multiple batches (max 200 fields per request). - const batches: string[][] = [[]]; - each(aggregatableFields, (field) => { - let lastArray: string[] = last(batches) as string[]; - if (lastArray.length === AGGREGATABLE_EXISTS_REQUEST_BATCH_SIZE) { - lastArray = []; - batches.push(lastArray); - } - lastArray.push(field); - }); - - await Promise.all( - batches.map(async (fields) => { - try { - const batchStats = await this.checkAggregatableFieldsExist( - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - undefined, - runtimeMappings - ); - - // Total count will be returned with each batch of fields. Just overwrite. - stats.totalCount = batchStats.totalCount; - - // Add to the lists of fields which do and do not exist. - stats.aggregatableExistsFields.push(...batchStats.aggregatableExistsFields); - stats.aggregatableNotExistsFields.push(...batchStats.aggregatableNotExistsFields); - } catch (e) { - // If index not found, no need to proceed with other batches - if (e.statusCode === 404) { - throw e; - } - stats.errors.push(wrapError(e)); - } - }) - ); - - await Promise.all( - nonAggregatableFields.map(async (field) => { - try { - const existsInDocs = await this.checkNonAggregatableFieldExists( - indexPatternTitle, - query, - field, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - - const fieldData: FieldData = { - fieldName: field, - existsInDocs, - stats: {}, - }; - - if (existsInDocs === true) { - stats.nonAggregatableExistsFields.push(fieldData); - } else { - stats.nonAggregatableNotExistsFields.push(fieldData); - } - } catch (e) { - stats.errors.push(wrapError(e)); - } - }) - ); - - return stats; - } - - // Obtains binned histograms for supplied list of fields. The statistics for each field in the - // returned array depend on the type of the field (keyword, number, date etc). - // Sampling will be used if supplied samplerShardSize > 0. - async getHistogramsForFields( - indexPatternTitle: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings?: estypes.MappingRuntimeFields - ): Promise { - return await getHistogramsForFields( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - runtimeMappings - ); - } - - // Obtains statistics for supplied list of fields. The statistics for each field in the - // returned array depend on the type of the field (keyword, number, date etc). - // Sampling will be used if supplied samplerShardSize > 0. - async getStatsForFields( - indexPatternTitle: string, - query: any, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - intervalMs: number | undefined, - maxExamples: number, - runtimeMappings: estypes.MappingRuntimeFields - ): Promise { - // Batch up fields by type, getting stats for multiple fields at a time. - const batches: Field[][] = []; - const batchedFields: { [key: string]: Field[][] } = {}; - each(fields, (field) => { - if (field.fieldName === undefined) { - // undefined fieldName is used for a document count request. - // getDocumentCountStats requires timeField - don't add to batched requests if not defined - if (timeFieldName !== undefined) { - batches.push([field]); - } - } else { - const fieldType = field.type; - if (batchedFields[fieldType] === undefined) { - batchedFields[fieldType] = [[]]; - } - let lastArray: Field[] = last(batchedFields[fieldType]) as Field[]; - if (lastArray.length === FIELDS_REQUEST_BATCH_SIZE) { - lastArray = []; - batchedFields[fieldType].push(lastArray); - } - lastArray.push(field); - } - }); - - each(batchedFields, (lists) => { - batches.push(...lists); - }); - - let results: BatchStats[] = []; - await Promise.all( - batches.map(async (batch) => { - let batchStats: BatchStats[] = []; - const first = batch[0]; - switch (first.type) { - case JOB_FIELD_TYPES.NUMBER: - // undefined fieldName is used for a document count request. - if (first.fieldName !== undefined) { - batchStats = await this.getNumericFieldsStats( - indexPatternTitle, - query, - batch, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } else { - // Will only ever be one document count card, - // so no value in batching up the single request. - if (intervalMs !== undefined) { - const stats = await this.getDocumentCountStats( - indexPatternTitle, - query, - timeFieldName, - earliestMs, - latestMs, - intervalMs, - runtimeMappings - ); - batchStats.push(stats); - } - } - break; - case JOB_FIELD_TYPES.KEYWORD: - case JOB_FIELD_TYPES.IP: - batchStats = await this.getStringFieldsStats( - indexPatternTitle, - query, - batch, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - break; - case JOB_FIELD_TYPES.DATE: - batchStats = await this.getDateFieldsStats( - indexPatternTitle, - query, - batch, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - break; - case JOB_FIELD_TYPES.BOOLEAN: - batchStats = await this.getBooleanFieldsStats( - indexPatternTitle, - query, - batch, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - break; - case JOB_FIELD_TYPES.TEXT: - default: - // Use an exists filter on the the field name to get - // examples of the field, so cannot batch up. - await Promise.all( - batch.map(async (field) => { - const stats = await this.getFieldExamples( - indexPatternTitle, - query, - field.fieldName, - timeFieldName, - earliestMs, - latestMs, - maxExamples, - runtimeMappings - ); - batchStats.push(stats); - }) - ); - break; - } - - results = [...results, ...batchStats]; - }) - ); - - return results; - } - - async checkAggregatableFieldsExist( - indexPatternTitle: string, - query: any, - aggregatableFields: string[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs?: number, - latestMs?: number, - datafeedConfig?: estypes.MlDatafeed, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await checkAggregatableFieldsExist( - this._client, - indexPatternTitle, - query, - aggregatableFields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - datafeedConfig, - runtimeMappings - ); - } - - async checkNonAggregatableFieldExists( - indexPatternTitle: string, - query: any, - field: string, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await checkNonAggregatableFieldExists( - this._client, - indexPatternTitle, - query, - field, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getDocumentCountStats( - indexPatternTitle: string, - query: any, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - intervalMs: number, - runtimeMappings: estypes.MappingRuntimeFields - ): Promise { - return await getDocumentCountStats( - this._client, - indexPatternTitle, - query, - timeFieldName, - earliestMs, - latestMs, - intervalMs, - runtimeMappings - ); - } - - async getNumericFieldsStats( - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await getNumericFieldsStats( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getStringFieldsStats( - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await getStringFieldsStats( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getDateFieldsStats( - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await getDateFieldsStats( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getBooleanFieldsStats( - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields - ) { - return await getBooleanFieldsStats( - this._client, - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); - } - - async getFieldExamples( - indexPatternTitle: string, - query: any, - field: string, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - maxExamples: number, - runtimeMappings?: estypes.MappingRuntimeFields - ): Promise { - return await getFieldExamples( - this._client, - indexPatternTitle, - query, - field, - timeFieldName, - earliestMs, - latestMs, - maxExamples, - runtimeMappings - ); - } -} diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_field_examples.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_field_examples.ts deleted file mode 100644 index 78adfb9e81b95..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_field_examples.ts +++ /dev/null @@ -1,80 +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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { get } from 'lodash'; -import { IScopedClusterClient } from 'kibana/server'; -import { buildBaseFilterCriteria } from '../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; -import { FieldExamples } from '../../types/chart_data'; - -export const getFieldExamples = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - field: string, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - maxExamples: number, - runtimeMappings?: estypes.MappingRuntimeFields -): Promise => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - - // Request at least 100 docs so that we have a chance of obtaining - // 'maxExamples' of the field. - const size = Math.max(100, maxExamples); - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - // Use an exists filter to return examples of the field. - filterCriteria.push({ - exists: { field }, - }); - - const searchBody = { - fields: [field], - _source: false, - query: { - bool: { - filter: filterCriteria, - }, - }, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const stats = { - fieldName: field, - examples: [] as any[], - }; - // @ts-expect-error incorrect search response type - if (body.hits.total.value > 0) { - const hits = body.hits.hits; - for (let i = 0; i < hits.length; i++) { - // Use lodash get() to support field names containing dots. - const doc: object[] | undefined = get(hits[i].fields, field); - // the results from fields query is always an array - if (Array.isArray(doc) && doc.length > 0) { - const example = doc[0]; - if (example !== undefined && stats.examples.indexOf(example) === -1) { - stats.examples.push(example); - if (stats.examples.length === maxExamples) { - break; - } - } - } - } - } - - return stats; -}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_fields_stats.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_fields_stats.ts deleted file mode 100644 index da93719e9ed93..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_fields_stats.ts +++ /dev/null @@ -1,478 +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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { each, find, get } from 'lodash'; -import { IScopedClusterClient } from 'kibana/server'; -import { - Aggs, - BooleanFieldStats, - Bucket, - DateFieldStats, - DocumentCountStats, - Field, - NumericFieldStats, - StringFieldStats, -} from '../../types'; -import { - buildBaseFilterCriteria, - buildSamplerAggregation, - getSafeAggregationName, - getSamplerAggregationsResponsePath, -} from '../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; -import { processDistributionData } from './process_distribution_data'; -import { SAMPLER_TOP_TERMS_SHARD_SIZE, SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; - -export const getDocumentCountStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - intervalMs: number, - runtimeMappings: estypes.MappingRuntimeFields -): Promise => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - // Don't use the sampler aggregation as this can lead to some potentially - // confusing date histogram results depending on the date range of data amongst shards. - - const aggs = { - eventRate: { - date_histogram: { - field: timeFieldName, - fixed_interval: `${intervalMs}ms`, - min_doc_count: 1, - }, - }, - }; - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - - const buckets: { [key: string]: number } = {}; - const dataByTimeBucket: Array<{ key: string; doc_count: number }> = get( - body, - ['aggregations', 'eventRate', 'buckets'], - [] - ); - each(dataByTimeBucket, (dataForTime) => { - const time = dataForTime.key; - buckets[time] = dataForTime.doc_count; - }); - - return { - documentCounts: { - interval: intervalMs, - buckets, - }, - }; -}; - -export const getNumericFieldsStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - // Build the percents parameter which defines the percentiles to query - // for the metric distribution data. - // Use a fixed percentile spacing of 5%. - const MAX_PERCENT = 100; - const PERCENTILE_SPACING = 5; - let count = 0; - const percents = Array.from( - Array(MAX_PERCENT / PERCENTILE_SPACING), - () => (count += PERCENTILE_SPACING) - ); - - const aggs: { [key: string]: any } = {}; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - aggs[`${safeFieldName}_field_stats`] = { - filter: { exists: { field: field.fieldName } }, - aggs: { - actual_stats: { - stats: { field: field.fieldName }, - }, - }, - }; - aggs[`${safeFieldName}_percentiles`] = { - percentiles: { - field: field.fieldName, - percents, - keyed: false, - }, - }; - - const top = { - terms: { - field: field.fieldName, - size: 10, - order: { - _count: 'desc', - }, - }, - }; - - // If cardinality >= SAMPLE_TOP_TERMS_THRESHOLD, run the top terms aggregation - // in a sampler aggregation, even if no sampling has been specified (samplerShardSize < 1). - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - aggs[`${safeFieldName}_top`] = { - sampler: { - shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE, - }, - aggs: { - top, - }, - }; - } else { - aggs[`${safeFieldName}_top`] = top; - } - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: buildSamplerAggregation(aggs, samplerShardSize), - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const aggregations = body.aggregations; - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const batchStats: NumericFieldStats[] = []; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - const docCount = get( - aggregations, - [...aggsPath, `${safeFieldName}_field_stats`, 'doc_count'], - 0 - ); - const fieldStatsResp = get( - aggregations, - [...aggsPath, `${safeFieldName}_field_stats`, 'actual_stats'], - {} - ); - - const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - topAggsPath.push('top'); - } - - const topValues: Bucket[] = get(aggregations, [...topAggsPath, 'buckets'], []); - - const stats: NumericFieldStats = { - fieldName: field.fieldName, - count: docCount, - min: get(fieldStatsResp, 'min', 0), - max: get(fieldStatsResp, 'max', 0), - avg: get(fieldStatsResp, 'avg', 0), - isTopValuesSampled: field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD || samplerShardSize > 0, - topValues, - topValuesSampleSize: topValues.reduce( - (acc, curr) => acc + curr.doc_count, - get(aggregations, [...topAggsPath, 'sum_other_doc_count'], 0) - ), - topValuesSamplerShardSize: - field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD - ? SAMPLER_TOP_TERMS_SHARD_SIZE - : samplerShardSize, - }; - - if (stats.count > 0) { - const percentiles = get( - aggregations, - [...aggsPath, `${safeFieldName}_percentiles`, 'values'], - [] - ); - const medianPercentile: { value: number; key: number } | undefined = find(percentiles, { - key: 50, - }); - stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; - stats.distribution = processDistributionData(percentiles, PERCENTILE_SPACING, stats.min); - } - - batchStats.push(stats); - }); - - return batchStats; -}; - -export const getStringFieldsStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - const aggs: Aggs = {}; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - const top = { - terms: { - field: field.fieldName, - size: 10, - order: { - _count: 'desc', - }, - }, - }; - - // If cardinality >= SAMPLE_TOP_TERMS_THRESHOLD, run the top terms aggregation - // in a sampler aggregation, even if no sampling has been specified (samplerShardSize < 1). - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - aggs[`${safeFieldName}_top`] = { - sampler: { - shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE, - }, - aggs: { - top, - }, - }; - } else { - aggs[`${safeFieldName}_top`] = top; - } - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: buildSamplerAggregation(aggs, samplerShardSize), - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const aggregations = body.aggregations; - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const batchStats: StringFieldStats[] = []; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - - const topAggsPath = [...aggsPath, `${safeFieldName}_top`]; - if (samplerShardSize < 1 && field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD) { - topAggsPath.push('top'); - } - - const topValues: Bucket[] = get(aggregations, [...topAggsPath, 'buckets'], []); - - const stats = { - fieldName: field.fieldName, - isTopValuesSampled: field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD || samplerShardSize > 0, - topValues, - topValuesSampleSize: topValues.reduce( - (acc, curr) => acc + curr.doc_count, - get(aggregations, [...topAggsPath, 'sum_other_doc_count'], 0) - ), - topValuesSamplerShardSize: - field.cardinality >= SAMPLER_TOP_TERMS_THRESHOLD - ? SAMPLER_TOP_TERMS_SHARD_SIZE - : samplerShardSize, - }; - - batchStats.push(stats); - }); - - return batchStats; -}; - -export const getDateFieldsStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - const aggs: Aggs = {}; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - aggs[`${safeFieldName}_field_stats`] = { - filter: { exists: { field: field.fieldName } }, - aggs: { - actual_stats: { - stats: { field: field.fieldName }, - }, - }, - }; - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: buildSamplerAggregation(aggs, samplerShardSize), - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const aggregations = body.aggregations; - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const batchStats: DateFieldStats[] = []; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - const docCount = get( - aggregations, - [...aggsPath, `${safeFieldName}_field_stats`, 'doc_count'], - 0 - ); - const fieldStatsResp = get( - aggregations, - [...aggsPath, `${safeFieldName}_field_stats`, 'actual_stats'], - {} - ); - batchStats.push({ - fieldName: field.fieldName, - count: docCount, - earliest: get(fieldStatsResp, 'min', 0), - latest: get(fieldStatsResp, 'max', 0), - }); - }); - - return batchStats; -}; - -export const getBooleanFieldsStats = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - - const index = indexPatternTitle; - const size = 0; - const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - - const aggs: Aggs = {}; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - aggs[`${safeFieldName}_value_count`] = { - filter: { exists: { field: field.fieldName } }, - }; - aggs[`${safeFieldName}_values`] = { - terms: { - field: field.fieldName, - size: 2, - }, - }; - }); - - const searchBody = { - query: { - bool: { - filter: filterCriteria, - }, - }, - aggs: buildSamplerAggregation(aggs, samplerShardSize), - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }; - - const { body } = await asCurrentUser.search({ - index, - size, - body: searchBody, - }); - const aggregations = body.aggregations; - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const batchStats: BooleanFieldStats[] = []; - fields.forEach((field, i) => { - const safeFieldName = getSafeAggregationName(field.fieldName, i); - const stats: BooleanFieldStats = { - fieldName: field.fieldName, - count: get(aggregations, [...aggsPath, `${safeFieldName}_value_count`, 'doc_count'], 0), - trueCount: 0, - falseCount: 0, - }; - - const valueBuckets: Array<{ [key: string]: number }> = get( - aggregations, - [...aggsPath, `${safeFieldName}_values`, 'buckets'], - [] - ); - valueBuckets.forEach((bucket) => { - stats[`${bucket.key_as_string}Count`] = bucket.doc_count; - }); - - batchStats.push(stats); - }); - - return batchStats; -}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_histogram_for_fields.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_histogram_for_fields.ts deleted file mode 100644 index 1cbf40a22b056..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/get_histogram_for_fields.ts +++ /dev/null @@ -1,186 +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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { get } from 'lodash'; -import { ChartData, ChartRequestAgg, HistogramField, NumericColumnStatsMap } from '../../types'; -import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; -import { stringHash } from '../../../common/utils/string_utils'; -import { - buildSamplerAggregation, - getSamplerAggregationsResponsePath, -} from '../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; -import { MAX_CHART_COLUMNS } from './constants'; - -export const getAggIntervals = async ( - { asCurrentUser }: IScopedClusterClient, - indexPatternTitle: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings?: estypes.MappingRuntimeFields -): Promise => { - const numericColumns = fields.filter((field) => { - return field.type === KBN_FIELD_TYPES.NUMBER || field.type === KBN_FIELD_TYPES.DATE; - }); - - if (numericColumns.length === 0) { - return {}; - } - - const minMaxAggs = numericColumns.reduce((aggs, c) => { - const id = stringHash(c.fieldName); - aggs[id] = { - stats: { - field: c.fieldName, - }, - }; - return aggs; - }, {} as Record); - - const { body } = await asCurrentUser.search({ - index: indexPatternTitle, - size: 0, - body: { - query, - aggs: buildSamplerAggregation(minMaxAggs, samplerShardSize), - size: 0, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }, - }); - - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; - - return Object.keys(aggregations).reduce((p, aggName) => { - const stats = [aggregations[aggName].min, aggregations[aggName].max]; - if (!stats.includes(null)) { - const delta = aggregations[aggName].max - aggregations[aggName].min; - - let aggInterval = 1; - - if (delta > MAX_CHART_COLUMNS || delta <= 1) { - aggInterval = delta / (MAX_CHART_COLUMNS - 1); - } - - p[aggName] = { interval: aggInterval, min: stats[0], max: stats[1] }; - } - - return p; - }, {} as NumericColumnStatsMap); -}; - -export const getHistogramsForFields = async ( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings?: estypes.MappingRuntimeFields -) => { - const { asCurrentUser } = client; - const aggIntervals = await getAggIntervals( - client, - indexPatternTitle, - query, - fields, - samplerShardSize, - runtimeMappings - ); - - const chartDataAggs = fields.reduce((aggs, field) => { - const fieldName = field.fieldName; - const fieldType = field.type; - const id = stringHash(fieldName); - if (fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE) { - if (aggIntervals[id] !== undefined) { - aggs[`${id}_histogram`] = { - histogram: { - field: fieldName, - interval: aggIntervals[id].interval !== 0 ? aggIntervals[id].interval : 1, - }, - }; - } - } else if (fieldType === KBN_FIELD_TYPES.STRING || fieldType === KBN_FIELD_TYPES.BOOLEAN) { - if (fieldType === KBN_FIELD_TYPES.STRING) { - aggs[`${id}_cardinality`] = { - cardinality: { - field: fieldName, - }, - }; - } - aggs[`${id}_terms`] = { - terms: { - field: fieldName, - size: MAX_CHART_COLUMNS, - }, - }; - } - return aggs; - }, {} as Record); - - if (Object.keys(chartDataAggs).length === 0) { - return []; - } - - const { body } = await asCurrentUser.search({ - index: indexPatternTitle, - size: 0, - body: { - query, - aggs: buildSamplerAggregation(chartDataAggs, samplerShardSize), - size: 0, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }, - }); - - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; - - const chartsData: ChartData[] = fields.map((field): ChartData => { - const fieldName = field.fieldName; - const fieldType = field.type; - const id = stringHash(field.fieldName); - - if (fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE) { - if (aggIntervals[id] === undefined) { - return { - type: 'numeric', - data: [], - interval: 0, - stats: [0, 0], - id: fieldName, - }; - } - - return { - data: aggregations[`${id}_histogram`].buckets, - interval: aggIntervals[id].interval, - stats: [aggIntervals[id].min, aggIntervals[id].max], - type: 'numeric', - id: fieldName, - }; - } else if (fieldType === KBN_FIELD_TYPES.STRING || fieldType === KBN_FIELD_TYPES.BOOLEAN) { - return { - type: fieldType === KBN_FIELD_TYPES.STRING ? 'ordinal' : 'boolean', - cardinality: - fieldType === KBN_FIELD_TYPES.STRING ? aggregations[`${id}_cardinality`].value : 2, - data: aggregations[`${id}_terms`].buckets, - id: fieldName, - }; - } - - return { - type: 'unsupported', - id: fieldName, - }; - }); - - return chartsData; -}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/index.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/index.ts deleted file mode 100644 index a29957b159b7e..0000000000000 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/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 './data_visualizer'; diff --git a/x-pack/plugins/data_visualizer/server/plugin.ts b/x-pack/plugins/data_visualizer/server/plugin.ts index e2e0637ef8f3f..9ef6ca5ae6a69 100644 --- a/x-pack/plugins/data_visualizer/server/plugin.ts +++ b/x-pack/plugins/data_visualizer/server/plugin.ts @@ -7,15 +7,12 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/server'; import { StartDeps, SetupDeps } from './types'; -import { dataVisualizerRoutes } from './routes'; import { registerWithCustomIntegrations } from './register_custom_integration'; export class DataVisualizerPlugin implements Plugin { constructor() {} setup(coreSetup: CoreSetup, plugins: SetupDeps) { - dataVisualizerRoutes(coreSetup); - // home-plugin required if (plugins.home && plugins.customIntegrations) { registerWithCustomIntegrations(plugins.customIntegrations); diff --git a/x-pack/plugins/data_visualizer/server/routes/index.ts b/x-pack/plugins/data_visualizer/server/routes/index.ts deleted file mode 100644 index 892f6cbd77361..0000000000000 --- a/x-pack/plugins/data_visualizer/server/routes/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 { dataVisualizerRoutes } from './routes'; diff --git a/x-pack/plugins/data_visualizer/server/routes/routes.ts b/x-pack/plugins/data_visualizer/server/routes/routes.ts deleted file mode 100644 index 1ec2eaa242c1c..0000000000000 --- a/x-pack/plugins/data_visualizer/server/routes/routes.ts +++ /dev/null @@ -1,262 +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 { CoreSetup, IScopedClusterClient } from 'kibana/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - dataVisualizerFieldHistogramsSchema, - dataVisualizerFieldStatsSchema, - dataVisualizerOverallStatsSchema, - dataViewTitleSchema, -} from './schemas'; -import type { Field, StartDeps, HistogramField } from '../types'; -import { DataVisualizer } from '../models/data_visualizer'; -import { wrapError } from '../utils/error_wrapper'; - -function getOverallStats( - client: IScopedClusterClient, - indexPatternTitle: string, - query: object, - aggregatableFields: string[], - nonAggregatableFields: string[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - runtimeMappings: estypes.MappingRuntimeFields -) { - const dv = new DataVisualizer(client); - return dv.getOverallStats( - indexPatternTitle, - query, - aggregatableFields, - nonAggregatableFields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); -} - -function getStatsForFields( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - fields: Field[], - samplerShardSize: number, - timeFieldName: string | undefined, - earliestMs: number | undefined, - latestMs: number | undefined, - interval: number | undefined, - maxExamples: number, - runtimeMappings: estypes.MappingRuntimeFields -) { - const dv = new DataVisualizer(client); - return dv.getStatsForFields( - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - interval, - maxExamples, - runtimeMappings - ); -} - -function getHistogramsForFields( - client: IScopedClusterClient, - indexPatternTitle: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings: estypes.MappingRuntimeFields -) { - const dv = new DataVisualizer(client); - return dv.getHistogramsForFields( - indexPatternTitle, - query, - fields, - samplerShardSize, - runtimeMappings - ); -} -/** - * Routes for the index data visualizer. - */ -export function dataVisualizerRoutes(coreSetup: CoreSetup) { - const router = coreSetup.http.createRouter(); - - /** - * @apiGroup DataVisualizer - * - * @api {post} /internal/data_visualizer/get_field_histograms/:dataViewTitle Get histograms for fields - * @apiName GetHistogramsForFields - * @apiDescription Returns the histograms on a list fields in the specified data view. - * - * @apiSchema (params) dataViewTitleSchema - * @apiSchema (body) dataVisualizerFieldHistogramsSchema - * - * @apiSuccess {Object} fieldName histograms by field, keyed on the name of the field. - */ - router.post( - { - path: '/internal/data_visualizer/get_field_histograms/{dataViewTitle}', - validate: { - params: dataViewTitleSchema, - body: dataVisualizerFieldHistogramsSchema, - }, - }, - async (context, request, response) => { - try { - const { - params: { dataViewTitle }, - body: { query, fields, samplerShardSize, runtimeMappings }, - } = request; - - const results = await getHistogramsForFields( - context.core.elasticsearch.client, - dataViewTitle, - query, - fields, - samplerShardSize, - runtimeMappings - ); - - return response.ok({ - body: results, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - } - ); - - /** - * @apiGroup DataVisualizer - * - * @api {post} /internal/data_visualizer/get_field_stats/:dataViewTitle Get stats for fields - * @apiName GetStatsForFields - * @apiDescription Returns the stats on individual fields in the specified data view. - * - * @apiSchema (params) dataViewTitleSchema - * @apiSchema (body) dataVisualizerFieldStatsSchema - * - * @apiSuccess {Object} fieldName stats by field, keyed on the name of the field. - */ - router.post( - { - path: '/internal/data_visualizer/get_field_stats/{dataViewTitle}', - validate: { - params: dataViewTitleSchema, - body: dataVisualizerFieldStatsSchema, - }, - }, - async (context, request, response) => { - try { - const { - params: { dataViewTitle }, - body: { - query, - fields, - samplerShardSize, - timeFieldName, - earliest, - latest, - interval, - maxExamples, - runtimeMappings, - }, - } = request; - const results = await getStatsForFields( - context.core.elasticsearch.client, - dataViewTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliest, - latest, - interval, - maxExamples, - runtimeMappings - ); - - return response.ok({ - body: results, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - } - ); - - /** - * @apiGroup DataVisualizer - * - * @api {post} /internal/data_visualizer/get_overall_stats/:dataViewTitle Get overall stats - * @apiName GetOverallStats - * @apiDescription Returns the top level overall stats for the specified data view. - * - * @apiSchema (params) dataViewTitleSchema - * @apiSchema (body) dataVisualizerOverallStatsSchema - * - * @apiSuccess {number} totalCount total count of documents. - * @apiSuccess {Object} aggregatableExistsFields stats on aggregatable fields that exist in documents. - * @apiSuccess {Object} aggregatableNotExistsFields stats on aggregatable fields that do not exist in documents. - * @apiSuccess {Object} nonAggregatableExistsFields stats on non-aggregatable fields that exist in documents. - * @apiSuccess {Object} nonAggregatableNotExistsFields stats on non-aggregatable fields that do not exist in documents. - */ - router.post( - { - path: '/internal/data_visualizer/get_overall_stats/{dataViewTitle}', - validate: { - params: dataViewTitleSchema, - body: dataVisualizerOverallStatsSchema, - }, - }, - async (context, request, response) => { - try { - const { - params: { dataViewTitle }, - body: { - query, - aggregatableFields, - nonAggregatableFields, - samplerShardSize, - timeFieldName, - earliest, - latest, - runtimeMappings, - }, - } = request; - - const results = await getOverallStats( - context.core.elasticsearch.client, - dataViewTitle, - query, - aggregatableFields, - nonAggregatableFields, - samplerShardSize, - timeFieldName, - earliest, - latest, - runtimeMappings - ); - - return response.ok({ - body: results, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - } - ); -} diff --git a/x-pack/plugins/data_visualizer/server/routes/schemas/index.ts b/x-pack/plugins/data_visualizer/server/routes/schemas/index.ts deleted file mode 100644 index 156336feef29e..0000000000000 --- a/x-pack/plugins/data_visualizer/server/routes/schemas/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 './index_data_visualizer_schemas'; diff --git a/x-pack/plugins/data_visualizer/server/routes/schemas/index_data_visualizer_schemas.ts b/x-pack/plugins/data_visualizer/server/routes/schemas/index_data_visualizer_schemas.ts deleted file mode 100644 index 3b5797622734f..0000000000000 --- a/x-pack/plugins/data_visualizer/server/routes/schemas/index_data_visualizer_schemas.ts +++ /dev/null @@ -1,76 +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 { schema } from '@kbn/config-schema'; -import { isRuntimeField } from '../../../common/utils/runtime_field_utils'; - -export const runtimeMappingsSchema = schema.object( - {}, - { - unknowns: 'allow', - validate: (v: object) => { - if (Object.values(v).some((o) => !isRuntimeField(o))) { - return 'Invalid runtime field'; - } - }, - } -); - -export const dataViewTitleSchema = schema.object({ - /** Title of the data view for which to return stats. */ - dataViewTitle: schema.string(), -}); - -export const dataVisualizerFieldHistogramsSchema = schema.object({ - /** Query to match documents in the index. */ - query: schema.any(), - /** The fields to return histogram data. */ - fields: schema.arrayOf(schema.any()), - /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ - samplerShardSize: schema.number(), - /** Optional search time runtime fields */ - runtimeMappings: runtimeMappingsSchema, -}); - -export const dataVisualizerFieldStatsSchema = schema.object({ - /** Query to match documents in the index. */ - query: schema.any(), - fields: schema.arrayOf(schema.any()), - /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ - samplerShardSize: schema.number(), - /** Name of the time field in the index (optional). */ - timeFieldName: schema.maybe(schema.string()), - /** Earliest timestamp for search, as epoch ms (optional). */ - earliest: schema.maybe(schema.number()), - /** Latest timestamp for search, as epoch ms (optional). */ - latest: schema.maybe(schema.number()), - /** Aggregation interval, in milliseconds, to use for obtaining document counts over time (optional). */ - interval: schema.maybe(schema.number()), - /** Maximum number of examples to return for text type fields. */ - maxExamples: schema.number(), - /** Optional search time runtime fields */ - runtimeMappings: runtimeMappingsSchema, -}); - -export const dataVisualizerOverallStatsSchema = schema.object({ - /** Query to match documents in the index. */ - query: schema.any(), - /** Names of aggregatable fields for which to return stats. */ - aggregatableFields: schema.arrayOf(schema.string()), - /** Names of non-aggregatable fields for which to return stats. */ - nonAggregatableFields: schema.arrayOf(schema.string()), - /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ - samplerShardSize: schema.number(), - /** Name of the time field in the index (optional). */ - timeFieldName: schema.maybe(schema.string()), - /** Earliest timestamp for search, as epoch ms (optional). */ - earliest: schema.maybe(schema.number()), - /** Latest timestamp for search, as epoch ms (optional). */ - latest: schema.maybe(schema.number()), - /** Optional search time runtime fields */ - runtimeMappings: runtimeMappingsSchema, -}); diff --git a/x-pack/plugins/data_visualizer/server/types/deps.ts b/x-pack/plugins/data_visualizer/server/types/deps.ts index 1f6dba0592f6f..8ee8c75abe543 100644 --- a/x-pack/plugins/data_visualizer/server/types/deps.ts +++ b/x-pack/plugins/data_visualizer/server/types/deps.ts @@ -9,12 +9,18 @@ import type { SecurityPluginStart } from '../../../security/server'; import type { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/server'; import { CustomIntegrationsPluginSetup } from '../../../../../src/plugins/custom_integrations/server'; import { HomeServerPluginSetup } from '../../../../../src/plugins/home/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../../src/plugins/data/server'; export interface StartDeps { security?: SecurityPluginStart; + data: DataPluginStart; } export interface SetupDeps { usageCollection: UsageCollectionSetup; customIntegrations?: CustomIntegrationsPluginSetup; home?: HomeServerPluginSetup; + data: DataPluginSetup; } diff --git a/x-pack/plugins/data_visualizer/server/types/index.ts b/x-pack/plugins/data_visualizer/server/types/index.ts index e0379b514de32..2fc0fb2a6173b 100644 --- a/x-pack/plugins/data_visualizer/server/types/index.ts +++ b/x-pack/plugins/data_visualizer/server/types/index.ts @@ -5,4 +5,3 @@ * 2.0. */ export * from './deps'; -export * from './chart_data'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5b4fe28ca5147..38bb3eb523d24 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8972,22 +8972,13 @@ "xpack.dataVisualizer.dataGrid.showDistributionsTooltip": "分布を表示", "xpack.dataVisualizer.dataGrid.typeColumnName": "型", "xpack.dataVisualizer.dataGridChart.histogramNotAvailable": "グラフはサポートされていません。", - "xpack.dataVisualizer.dataGridChart.notEnoughData": "0個のドキュメントにフィールドが含まれます。", "xpack.dataVisualizer.dataGridChart.topCategoriesLegend": "上位 {maxChartColumns}/{cardinality} カテゴリ", "xpack.dataVisualizer.description": "CSV、NDJSON、またはログファイルをインポートします。", "xpack.dataVisualizer.fieldNameSelect": "フィールド名", "xpack.dataVisualizer.fieldStats.maxTitle": "最高", "xpack.dataVisualizer.fieldStats.medianTitle": "中間", "xpack.dataVisualizer.fieldStats.minTitle": "分", - "xpack.dataVisualizer.fieldTypeIcon.booleanTypeAriaLabel": "ブールタイプ", - "xpack.dataVisualizer.fieldTypeIcon.dateTypeAriaLabel": "日付タイプ", "xpack.dataVisualizer.fieldTypeIcon.fieldTypeTooltip": "{type} タイプ", - "xpack.dataVisualizer.fieldTypeIcon.geoPointTypeAriaLabel": "{geoPointParam} タイプ", - "xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel": "IP タイプ", - "xpack.dataVisualizer.fieldTypeIcon.keywordTypeAriaLabel": "キーワードタイプ", - "xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel": "数字タイプ", - "xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel": "テキストタイプ", - "xpack.dataVisualizer.fieldTypeIcon.unknownTypeAriaLabel": "不明なタイプ", "xpack.dataVisualizer.fieldTypeSelect": "フィールド型", "xpack.dataVisualizer.file.aboutPanel.analyzingDataTitle": "データを分析中", "xpack.dataVisualizer.file.aboutPanel.selectOrDragAndDropFileDescription": "ファイルを選択するかドラッグ & ドロップしてください", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f166dc1d92c59..d5a2172e24104 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9055,7 +9055,6 @@ "xpack.dataVisualizer.dataGrid.showDistributionsTooltip": "显示分布", "xpack.dataVisualizer.dataGrid.typeColumnName": "类型", "xpack.dataVisualizer.dataGridChart.histogramNotAvailable": "不支持图表。", - "xpack.dataVisualizer.dataGridChart.notEnoughData": "0 个文档包含字段。", "xpack.dataVisualizer.dataGridChart.singleCategoryLegend": "{cardinality, plural, other {# 个类别}}", "xpack.dataVisualizer.dataGridChart.topCategoriesLegend": "{cardinality} 个类别中的排名前 {maxChartColumns} 个", "xpack.dataVisualizer.description": "导入您自己的 CSV、NDJSON 或日志文件。", @@ -9063,15 +9062,7 @@ "xpack.dataVisualizer.fieldStats.maxTitle": "最大值", "xpack.dataVisualizer.fieldStats.medianTitle": "中值", "xpack.dataVisualizer.fieldStats.minTitle": "最小值", - "xpack.dataVisualizer.fieldTypeIcon.booleanTypeAriaLabel": "布尔类型", - "xpack.dataVisualizer.fieldTypeIcon.dateTypeAriaLabel": "日期类型", "xpack.dataVisualizer.fieldTypeIcon.fieldTypeTooltip": "{type} 类型", - "xpack.dataVisualizer.fieldTypeIcon.geoPointTypeAriaLabel": "{geoPointParam} 类型", - "xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel": "IP 类型", - "xpack.dataVisualizer.fieldTypeIcon.keywordTypeAriaLabel": "关键字类型", - "xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel": "数字类型", - "xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel": "文本类型", - "xpack.dataVisualizer.fieldTypeIcon.unknownTypeAriaLabel": "未知类型", "xpack.dataVisualizer.fieldTypeSelect": "字段类型", "xpack.dataVisualizer.file.aboutPanel.analyzingDataTitle": "正在分析数据", "xpack.dataVisualizer.file.aboutPanel.selectOrDragAndDropFileDescription": "选择或拖放文件", diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts deleted file mode 100644 index 488df74b46968..0000000000000 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { USER } from '../../../../functional/services/ml/security_common'; -import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; - -export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertestWithoutAuth'); - const ml = getService('ml'); - - const fieldHistogramsTestData = { - testTitle: 'returns histogram data for fields', - index: 'ft_farequote', - user: USER.ML_POWERUSER, - requestBody: { - query: { bool: { should: [{ match_phrase: { airline: 'JZA' } }], minimum_should_match: 1 } }, - fields: [ - { fieldName: '@timestamp', type: 'date' }, - { fieldName: 'airline', type: 'string' }, - { fieldName: 'responsetime', type: 'number' }, - ], - samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. - }, - expected: { - responseCode: 200, - responseBody: [ - { - dataLength: 20, - type: 'numeric', - id: '@timestamp', - }, - { type: 'ordinal', dataLength: 1, id: 'airline' }, - { - dataLength: 20, - type: 'numeric', - id: 'responsetime', - }, - ], - }, - }; - - const errorTestData = { - testTitle: 'returns error for index which does not exist', - index: 'ft_farequote_not_exists', - user: USER.ML_POWERUSER, - requestBody: { - query: { bool: { must: [{ match_all: {} }] } }, - fields: [{ fieldName: 'responsetime', type: 'number' }], - samplerShardSize: -1, - }, - expected: { - responseCode: 404, - responseBody: { - statusCode: 404, - error: 'Not Found', - message: 'index_not_found_exception', - }, - }, - }; - - async function runGetFieldHistogramsRequest( - index: string, - user: USER, - requestBody: object, - expectedResponsecode: number - ): Promise { - const { body } = await supertest - .post(`/api/ml/data_visualizer/get_field_histograms/${index}`) - .auth(user, ml.securityCommon.getPasswordForUser(user)) - .set(COMMON_REQUEST_HEADERS) - .send(requestBody) - .expect(expectedResponsecode); - - return body; - } - - describe('get_field_histograms', function () { - before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); - await ml.testResources.setKibanaTimeZoneToUTC(); - }); - - it(`${fieldHistogramsTestData.testTitle}`, async () => { - const body = await runGetFieldHistogramsRequest( - fieldHistogramsTestData.index, - fieldHistogramsTestData.user, - fieldHistogramsTestData.requestBody, - fieldHistogramsTestData.expected.responseCode - ); - - const expected = fieldHistogramsTestData.expected; - - const actual = body.map((b: any) => ({ - dataLength: b.data.length, - type: b.type, - id: b.id, - })); - expect(actual).to.eql(expected.responseBody); - }); - - it(`${errorTestData.testTitle}`, async () => { - const body = await runGetFieldHistogramsRequest( - errorTestData.index, - errorTestData.user, - errorTestData.requestBody, - errorTestData.expected.responseCode - ); - - expect(body.error).to.eql(errorTestData.expected.responseBody.error); - expect(body.message).to.contain(errorTestData.expected.responseBody.message); - }); - }); -}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts deleted file mode 100644 index 65fd1c1ff0c85..0000000000000 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { sortBy } from 'lodash'; - -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { USER } from '../../../../functional/services/ml/security_common'; -import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; - -export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertestWithoutAuth'); - const ml = getService('ml'); - - const metricFieldsTestData = { - testTitle: 'returns stats for metric fields over all time', - index: 'ft_farequote', - user: USER.ML_POWERUSER, - requestBody: { - query: { - bool: { - must: { - term: { airline: 'JZA' }, // Only use one airline to ensure no sampling. - }, - }, - }, - fields: [ - { type: 'number', cardinality: 0 }, - { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, - ], - samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. - timeFieldName: '@timestamp', - interval: 86400000, - maxExamples: 10, - }, - expected: { - responseCode: 200, - responseBody: [ - { - documentCounts: { - interval: 86400000, - buckets: { - '1454803200000': 846, - '1454889600000': 846, - '1454976000000': 859, - '1455062400000': 851, - '1455148800000': 858, - }, - }, - }, - { - // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. - fieldName: 'responsetime', - count: 4260, - min: 963.4293212890625, - max: 1042.13525390625, - avg: 1000.0378077547315, - isTopValuesSampled: false, - topValues: [ - { key: 980.0411987304688, doc_count: 2 }, - { key: 989.278076171875, doc_count: 2 }, - { key: 989.763916015625, doc_count: 2 }, - { key: 991.290771484375, doc_count: 2 }, - { key: 992.0765991210938, doc_count: 2 }, - { key: 993.8115844726562, doc_count: 2 }, - { key: 993.8973999023438, doc_count: 2 }, - { key: 994.0230102539062, doc_count: 2 }, - { key: 994.364990234375, doc_count: 2 }, - { key: 994.916015625, doc_count: 2 }, - ], - topValuesSampleSize: 4260, - topValuesSamplerShardSize: -1, - }, - ], - }, - }; - - const nonMetricFieldsTestData = { - testTitle: 'returns stats for non-metric fields specifying query and time range', - index: 'ft_farequote', - user: USER.ML_POWERUSER, - requestBody: { - query: { - bool: { - must: { - term: { airline: 'AAL' }, - }, - }, - }, - fields: [ - { fieldName: '@timestamp', type: 'date', cardinality: 4751 }, - { fieldName: '@version.keyword', type: 'keyword', cardinality: 1 }, - { fieldName: 'airline', type: 'keyword', cardinality: 19 }, - { fieldName: 'type', type: 'text', cardinality: 0 }, - { fieldName: 'type.keyword', type: 'keyword', cardinality: 1 }, - ], - samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. - timeFieldName: '@timestamp', - earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT - latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT - maxExamples: 10, - }, - expected: { - responseCode: 200, - responseBody: [ - { fieldName: '@timestamp', count: 1733, earliest: 1454889602000, latest: 1454975948000 }, - { - fieldName: '@version.keyword', - isTopValuesSampled: false, - topValues: [{ key: '1', doc_count: 1733 }], - topValuesSampleSize: 1733, - topValuesSamplerShardSize: -1, - }, - { - fieldName: 'airline', - isTopValuesSampled: false, - topValues: [{ key: 'AAL', doc_count: 1733 }], - topValuesSampleSize: 1733, - topValuesSamplerShardSize: -1, - }, - { - fieldName: 'type.keyword', - isTopValuesSampled: false, - topValues: [{ key: 'farequote', doc_count: 1733 }], - topValuesSampleSize: 1733, - topValuesSamplerShardSize: -1, - }, - { fieldName: 'type', examples: ['farequote'] }, - ], - }, - }; - - const errorTestData = { - testTitle: 'returns error for index which does not exist', - index: 'ft_farequote_not_exists', - user: USER.ML_POWERUSER, - requestBody: { - query: { bool: { must: [{ match_all: {} }] } }, - fields: [ - { type: 'number', cardinality: 0 }, - { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, - ], - samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. - timeFieldName: '@timestamp', - interval: 86400000, - maxExamples: 10, - }, - expected: { - responseCode: 404, - responseBody: { - statusCode: 404, - error: 'Not Found', - message: 'index_not_found_exception', - }, - }, - }; - - async function runGetFieldStatsRequest( - index: string, - user: USER, - requestBody: object, - expectedResponsecode: number - ): Promise { - const { body } = await supertest - .post(`/internal/data_visualizer/get_field_stats/${index}`) - .auth(user, ml.securityCommon.getPasswordForUser(user)) - .set(COMMON_REQUEST_HEADERS) - .send(requestBody) - .expect(expectedResponsecode); - - return body; - } - - // Move these tests to file_data_visualizer plugin - describe('get_field_stats', function () { - before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); - await ml.testResources.setKibanaTimeZoneToUTC(); - }); - - it(`${metricFieldsTestData.testTitle}`, async () => { - const body = await runGetFieldStatsRequest( - metricFieldsTestData.index, - metricFieldsTestData.user, - metricFieldsTestData.requestBody, - metricFieldsTestData.expected.responseCode - ); - - // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. - const expected = metricFieldsTestData.expected; - expect(body).to.have.length(expected.responseBody.length); - - const actualDocCounts = body[0]; - const expectedDocCounts = expected.responseBody[0]; - expect(actualDocCounts).to.eql(expectedDocCounts); - - const actualFieldData = { ...body[1] }; - delete actualFieldData.median; - delete actualFieldData.distribution; - - expect(actualFieldData).to.eql(expected.responseBody[1]); - }); - - it(`${nonMetricFieldsTestData.testTitle}`, async () => { - const body = await runGetFieldStatsRequest( - nonMetricFieldsTestData.index, - nonMetricFieldsTestData.user, - nonMetricFieldsTestData.requestBody, - nonMetricFieldsTestData.expected.responseCode - ); - - const expectedRspFields = sortBy(nonMetricFieldsTestData.expected.responseBody, 'fieldName'); - const actualRspFields = sortBy(body, 'fieldName'); - expect(actualRspFields).to.eql(expectedRspFields); - }); - - it(`${errorTestData.testTitle}`, async () => { - const body = await runGetFieldStatsRequest( - errorTestData.index, - errorTestData.user, - errorTestData.requestBody, - errorTestData.expected.responseCode - ); - - expect(body.error).to.eql(errorTestData.expected.responseBody.error); - expect(body.message).to.contain(errorTestData.expected.responseBody.message); - }); - }); -}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts deleted file mode 100644 index 7987875a75519..0000000000000 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { USER } from '../../../../functional/services/ml/security_common'; -import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; - -export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertestWithoutAuth'); - const ml = getService('ml'); - - const testDataList = [ - { - testTitle: 'returns stats over all time', - index: 'ft_farequote', - user: USER.ML_POWERUSER, - requestBody: { - query: { bool: { must: [{ match_all: {} }] } }, - aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], - nonAggregatableFields: ['type'], - samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. - timeFieldName: '@timestamp', - }, - expected: { - responseCode: 200, - responseBody: { - totalCount: 86274, - aggregatableExistsFields: [ - { - fieldName: '@timestamp', - existsInDocs: true, - stats: { sampleCount: 86274, count: 86274, cardinality: 78580 }, - }, - { - fieldName: 'airline', - existsInDocs: true, - stats: { sampleCount: 86274, count: 86274, cardinality: 19 }, - }, - { - fieldName: 'responsetime', - existsInDocs: true, - stats: { sampleCount: 86274, count: 86274, cardinality: 83346 }, - }, - ], - aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], - nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], - nonAggregatableNotExistsFields: [], - errors: [], - }, - }, - }, - { - testTitle: 'returns stats when specifying query and time range', - index: 'ft_farequote', - user: USER.ML_POWERUSER, - requestBody: { - query: { - bool: { - must: { - term: { airline: 'AAL' }, - }, - }, - }, - aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], - nonAggregatableFields: ['type'], - samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. - timeFieldName: '@timestamp', - earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT - latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT - }, - expected: { - responseCode: 200, - responseBody: { - totalCount: 1733, - aggregatableExistsFields: [ - { - fieldName: '@timestamp', - existsInDocs: true, - stats: { sampleCount: 1733, count: 1733, cardinality: 1713 }, - }, - { - fieldName: 'airline', - existsInDocs: true, - stats: { sampleCount: 1733, count: 1733, cardinality: 1 }, - }, - { - fieldName: 'responsetime', - existsInDocs: true, - stats: { sampleCount: 1733, count: 1733, cardinality: 1730 }, - }, - ], - aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], - nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], - nonAggregatableNotExistsFields: [], - errors: [], - }, - }, - }, - { - testTitle: 'returns error for index which does not exist', - index: 'ft_farequote_not_exist', - user: USER.ML_POWERUSER, - requestBody: { - query: { bool: { must: [{ match_all: {} }] } }, - aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], - nonAggregatableFields: ['@version', 'type'], - samplerShardSize: 1000, - timeFieldName: '@timestamp', - }, - expected: { - responseCode: 404, - responseBody: { - statusCode: 404, - error: 'Not Found', - message: 'index_not_found_exception', - }, - }, - }, - ]; - - // Move these tests to file_data_visualizer plugin - describe('get_overall_stats', function () { - before(async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); - await ml.testResources.setKibanaTimeZoneToUTC(); - }); - - for (const testData of testDataList) { - it(`${testData.testTitle}`, async () => { - const { body } = await supertest - .post(`/internal/data_visualizer/get_overall_stats/${testData.index}`) - .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) - .set(COMMON_REQUEST_HEADERS) - .send(testData.requestBody) - .expect(testData.expected.responseCode); - - if (body.error === undefined) { - expect(body).to.eql(testData.expected.responseBody); - } else { - expect(body.error).to.eql(testData.expected.responseBody.error); - expect(body.message).to.contain(testData.expected.responseBody.message); - } - }); - } - }); -}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts deleted file mode 100644 index 3865247979a4a..0000000000000 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts +++ /dev/null @@ -1,15 +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 { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('data visualizer', function () { - loadTestFile(require.resolve('./get_field_stats')); - loadTestFile(require.resolve('./get_overall_stats')); - }); -} diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 06910e8fac67e..f276cbe5355ca 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -74,7 +74,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./calendars')); loadTestFile(require.resolve('./datafeeds')); loadTestFile(require.resolve('./data_frame_analytics')); - loadTestFile(require.resolve('./data_visualizer')); loadTestFile(require.resolve('./fields_service')); loadTestFile(require.resolve('./filters')); loadTestFile(require.resolve('./indices')); diff --git a/x-pack/test/api_integration_basic/apis/index.ts b/x-pack/test/api_integration_basic/apis/index.ts index 27869095bd792..9490d4c277675 100644 --- a/x-pack/test/api_integration_basic/apis/index.ts +++ b/x-pack/test/api_integration_basic/apis/index.ts @@ -11,7 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { this.tags('ciGroup11'); - loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./security_solution')); }); diff --git a/x-pack/test/api_integration_basic/apis/ml/data_visualizer/index.ts b/x-pack/test/api_integration_basic/apis/ml/data_visualizer/index.ts deleted file mode 100644 index 85b462a01760b..0000000000000 --- a/x-pack/test/api_integration_basic/apis/ml/data_visualizer/index.ts +++ /dev/null @@ -1,15 +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 { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('data visualizer', function () { - // The data visualizer APIs should work the same as with a trial license - loadTestFile(require.resolve('../../../../api_integration/apis/ml/data_visualizer')); - }); -} diff --git a/x-pack/test/api_integration_basic/apis/ml/index.ts b/x-pack/test/api_integration_basic/apis/ml/index.ts deleted file mode 100644 index 5ca70103f41eb..0000000000000 --- a/x-pack/test/api_integration_basic/apis/ml/index.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. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const ml = getService('ml'); - - describe('machine learning basic license', function () { - this.tags(['mlqa']); - - before(async () => { - await ml.securityCommon.createMlRoles(); - await ml.securityCommon.createMlUsers(); - }); - - after(async () => { - await ml.securityCommon.cleanMlUsers(); - await ml.securityCommon.cleanMlRoles(); - - await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); - - await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); - - await ml.testResources.resetKibanaTimeZone(); - }); - - loadTestFile(require.resolve('./data_visualizer')); - }); -} diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts index c5461e3bb9c21..69a1edb403369 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts @@ -7,23 +7,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; -import { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types'; - -interface MetricFieldVisConfig extends FieldVisConfig { - statsMaxDecimalPlaces: number; - docCountFormatted: string; - topValuesCount: number; - viewableInLens: boolean; - hasActionMenu: boolean; -} - -interface NonMetricFieldVisConfig extends FieldVisConfig { - docCountFormatted: string; - exampleCount: number; - viewableInLens: boolean; - hasActionMenu: boolean; -} - +import { MetricFieldVisConfig, NonMetricFieldVisConfig } from './types'; interface TestData { suiteTitle: string; sourceIndexOrSavedSearch: string; diff --git a/x-pack/test/functional/apps/ml/data_visualizer/types.ts b/x-pack/test/functional/apps/ml/data_visualizer/types.ts index dc38bc31f568b..a5a016289b461 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/types.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/types.ts @@ -5,20 +5,24 @@ * 2.0. */ -import { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types'; +import type { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types'; export interface MetricFieldVisConfig extends FieldVisConfig { + fieldName: string; statsMaxDecimalPlaces: number; docCountFormatted: string; topValuesCount: number; viewableInLens: boolean; + hasActionMenu?: boolean; } export interface NonMetricFieldVisConfig extends FieldVisConfig { + fieldName: string; docCountFormatted: string; exampleCount: number; exampleContent?: string[]; viewableInLens: boolean; + hasActionMenu?: boolean; } export interface TestData { From f68d5ad7529b7dd4fe90b6739ff5dd0237e9e5f6 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 8 Nov 2021 14:17:16 -0700 Subject: [PATCH 13/98] [maps] fix layer flashes when query is updated in query bar for mvt layers (#117590) * [maps] fix layer flashes when query is updated in query bar for mvt layers * remove unused services * eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../routes/map_page/map_app/map_app.tsx | 19 +- .../test/functional/apps/maps/mvt_scaling.js | 335 ++++++++++++++---- .../fixtures/kbn_archiver/maps.json | 25 ++ 3 files changed, 299 insertions(+), 80 deletions(-) diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 3825c92f31371..7765b3467a805 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -207,12 +207,10 @@ export class MapApp extends React.Component { filters, query, time, - forceRefresh = false, }: { filters?: Filter[]; query?: Query; time?: TimeRange; - forceRefresh?: boolean; }) => { const { filterManager } = getData().query; @@ -221,7 +219,7 @@ export class MapApp extends React.Component { } this.props.setQuery({ - forceRefresh, + forceRefresh: false, filters: filterManager.getFilters(), query, timeFilters: time, @@ -398,11 +396,16 @@ export class MapApp extends React.Component { filters={this.props.filters} query={this.props.query} onQuerySubmit={({ dateRange, query }) => { - this._onQueryChange({ - query, - time: dateRange, - forceRefresh: true, - }); + const isUpdate = + !_.isEqual(dateRange, this.props.timeFilters) || !_.isEqual(query, this.props.query); + if (isUpdate) { + this._onQueryChange({ + query, + time: dateRange, + }); + } else { + this.props.setQuery({ forceRefresh: true }); + } }} onFiltersUpdated={this._onFiltersChange} dateRangeFrom={this.props.timeFilters.from} diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/mvt_scaling.js index d9b660ba0d730..87dab5a2e3ad4 100644 --- a/x-pack/test/functional/apps/maps/mvt_scaling.js +++ b/x-pack/test/functional/apps/maps/mvt_scaling.js @@ -7,17 +7,18 @@ import expect from '@kbn/expect'; -const VECTOR_SOURCE_ID = 'caffa63a-ebfb-466d-8ff6-d797975b88ab'; - export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const inspector = getService('inspector'); const security = getService('security'); + const testSubjects = getService('testSubjects'); - describe('mvt geoshape layer', () => { + describe('mvt scaling', () => { before(async () => { - await security.testUser.setRoles(['global_maps_all', 'geoshape_data_reader'], false); - await PageObjects.maps.loadSavedMap('geo_shape_mvt'); + await security.testUser.setRoles( + ['global_maps_all', 'test_logstash_reader', 'geoshape_data_reader'], + false + ); }); after(async () => { @@ -25,85 +26,275 @@ export default function ({ getPageObjects, getService }) { await security.testUser.restoreDefaults(); }); - it('should render with mvt-source', async () => { - const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - - //Source should be correct - expect( - mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0].startsWith( - `/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))` - ) - ).to.equal(true); - - //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) - const fillLayer = mapboxStyle.layers.find((layer) => layer.id === VECTOR_SOURCE_ID + '_fill'); - expect(fillLayer.paint).to.eql({ - 'fill-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', + describe('layer style', () => { + const VECTOR_SOURCE_ID = 'caffa63a-ebfb-466d-8ff6-d797975b88ab'; + + let mapboxStyle; + before(async () => { + await PageObjects.maps.loadSavedMap('geo_shape_mvt'); + mapboxStyle = await PageObjects.maps.getMapboxStyle(); + }); + + it('should request tiles from /api/maps/mvt/getTile', async () => { + const tileUrl = new URL( + mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0], + 'http://absolute_path' + ); + const searchParams = Object.fromEntries(tileUrl.searchParams); + + expect(tileUrl.pathname).to.equal('/api/maps/mvt/getTile/%7Bz%7D/%7Bx%7D/%7By%7D.pbf'); + + // token is an unique id that changes between runs + expect(typeof searchParams.token).to.equal('string'); + delete searchParams.token; + + expect(searchParams).to.eql({ + geometryFieldName: 'geometry', + index: 'geo_shapes*', + requestBody: + '(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))', + }); + }); + + it('should have fill layer', async () => { + //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) + const fillLayer = mapboxStyle.layers.find( + (layer) => layer.id === VECTOR_SOURCE_ID + '_fill' + ); + expect(fillLayer.paint).to.eql({ + 'fill-color': [ + 'interpolate', + ['linear'], [ - 'case', - ['==', ['get', 'prop1'], null], - 0.3819660112501051, + 'coalesce', [ - 'max', - ['min', ['to-number', ['get', 'prop1']], 3.618033988749895], - 1.381966011250105, + 'case', + ['==', ['get', 'prop1'], null], + 0.3819660112501051, + [ + 'max', + ['min', ['to-number', ['get', 'prop1']], 3.618033988749895], + 1.381966011250105, + ], ], + 0.3819660112501051, ], 0.3819660112501051, + 'rgba(0,0,0,0)', + 1.381966011250105, + '#ecf1f7', + 1.6614745084375788, + '#d9e3ef', + 1.9409830056250525, + '#c5d5e7', + 2.2204915028125263, + '#b2c7df', + 2.5, + '#9eb9d8', + 2.7795084971874737, + '#8bacd0', + 3.0590169943749475, + '#769fc8', + 3.338525491562421, + '#6092c0', ], - 0.3819660112501051, - 'rgba(0,0,0,0)', - 1.381966011250105, - '#ecf1f7', - 1.6614745084375788, - '#d9e3ef', - 1.9409830056250525, - '#c5d5e7', - 2.2204915028125263, - '#b2c7df', - 2.5, - '#9eb9d8', - 2.7795084971874737, - '#8bacd0', - 3.0590169943749475, - '#769fc8', - 3.338525491562421, - '#6092c0', - ], - 'fill-opacity': 1, + 'fill-opacity': 1, + }); + }); + + it('should have toomanyfeatures layer', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === `${VECTOR_SOURCE_ID}_toomanyfeatures`; + }); + + expect(layer).to.eql({ + id: 'caffa63a-ebfb-466d-8ff6-d797975b88ab_toomanyfeatures', + type: 'line', + source: 'caffa63a-ebfb-466d-8ff6-d797975b88ab', + 'source-layer': 'meta', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', 'hits.total.relation'], 'gte'], + ['>=', ['get', 'hits.total.value'], 10002], + ], + layout: { visibility: 'visible' }, + paint: { + 'line-color': '#fec514', + 'line-width': 3, + 'line-dasharray': [2, 1], + 'line-opacity': 1, + }, + }); }); }); - it('Style should include toomanyfeatures layer', async () => { - const mapboxStyle = await PageObjects.maps.getMapboxStyle(); + describe('filtering', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('MVT documents'); + }); + + async function getTileUrl() { + const mapboxStyle = await PageObjects.maps.getMapboxStyle(); + return mapboxStyle.sources['a7ab2e06-145b-48c5-bd86-b633849017ad'].tiles[0]; + } + + describe('applyGlobalQuery: true, applyGlobalTime: true, applyForceRefresh: true', () => { + after(async () => { + await PageObjects.maps.setAbsoluteRange( + 'Sep 20, 2015 @ 00:00:00.000', + 'Sep 20, 2015 @ 01:00:00.000' + ); + await PageObjects.maps.setAndSubmitQuery(''); + }); - const layer = mapboxStyle.layers.find((mbLayer) => { - return mbLayer.id === `${VECTOR_SOURCE_ID}_toomanyfeatures`; + it('should update MVT URL when query changes', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"'); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); + + it('should update MVT URL when time changes', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.setAbsoluteRange( + 'Sep 20, 2015 @ 00:00:00.000', + 'Sep 20, 2015 @ 03:00:00.000' + ); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); + + it('should update MVT URL when refresh clicked', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.refreshQuery(); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); + }); + + describe('applyGlobalQuery: false, applyGlobalTime: true, applyForceRefresh: true', () => { + before(async () => { + await PageObjects.maps.openLayerPanel('logstash-*'); + await testSubjects.click('mapLayerPanelApplyGlobalQueryCheckbox'); + await PageObjects.maps.waitForLayersToLoad(); + }); + + after(async () => { + await PageObjects.maps.closeLayerPanel(); + await PageObjects.maps.setAbsoluteRange( + 'Sep 20, 2015 @ 00:00:00.000', + 'Sep 20, 2015 @ 01:00:00.000' + ); + await PageObjects.maps.setAndSubmitQuery(''); + }); + + it('should not update MVT URL when query changes', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"'); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.eql(nextTileUrl); + }); + + it('should update MVT URL when time changes', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.setAbsoluteRange( + 'Sep 20, 2015 @ 00:00:00.000', + 'Sep 20, 2015 @ 03:00:00.000' + ); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); + + it('should update MVT URL when refresh clicked', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.refreshQuery(); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); + }); + + describe('applyGlobalQuery: true, applyGlobalTime: false, applyForceRefresh: true', () => { + before(async () => { + await PageObjects.maps.openLayerPanel('logstash-*'); + await testSubjects.click('mapLayerPanelApplyGlobalTimeCheckbox'); + await PageObjects.maps.waitForLayersToLoad(); + }); + + after(async () => { + await PageObjects.maps.closeLayerPanel(); + await PageObjects.maps.setAbsoluteRange( + 'Sep 20, 2015 @ 00:00:00.000', + 'Sep 20, 2015 @ 01:00:00.000' + ); + await PageObjects.maps.setAndSubmitQuery(''); + }); + + it('should update MVT URL when query changes', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"'); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); + + it('should not update MVT URL when time changes', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.setAbsoluteRange( + 'Sep 20, 2015 @ 00:00:00.000', + 'Sep 20, 2015 @ 03:00:00.000' + ); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.eql(nextTileUrl); + }); + + it('should update MVT URL when refresh clicked', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.refreshQuery(); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); }); - expect(layer).to.eql({ - id: 'caffa63a-ebfb-466d-8ff6-d797975b88ab_toomanyfeatures', - type: 'line', - source: 'caffa63a-ebfb-466d-8ff6-d797975b88ab', - 'source-layer': 'meta', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', 'hits.total.relation'], 'gte'], - ['>=', ['get', 'hits.total.value'], 10002], - ], - layout: { visibility: 'visible' }, - paint: { - 'line-color': '#fec514', - 'line-width': 3, - 'line-dasharray': [2, 1], - 'line-opacity': 1, - }, + describe('applyGlobalQuery: true, applyGlobalTime: true, applyForceRefresh: false', () => { + before(async () => { + await PageObjects.maps.openLayerPanel('logstash-*'); + await testSubjects.click('mapLayerPanelRespondToForceRefreshCheckbox'); + await PageObjects.maps.waitForLayersToLoad(); + }); + + after(async () => { + await PageObjects.maps.closeLayerPanel(); + await PageObjects.maps.setAbsoluteRange( + 'Sep 20, 2015 @ 00:00:00.000', + 'Sep 20, 2015 @ 01:00:00.000' + ); + await PageObjects.maps.setAndSubmitQuery(''); + }); + + it('should update MVT URL when query changes', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"'); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); + + it('should update MVT URL when time changes', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.setAbsoluteRange( + 'Sep 20, 2015 @ 00:00:00.000', + 'Sep 20, 2015 @ 03:00:00.000' + ); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.not.eql(nextTileUrl); + }); + + it('should not update MVT URL when refresh clicked', async () => { + const prevTileUrl = await getTileUrl(); + await PageObjects.maps.refreshQuery(); + const nextTileUrl = await getTileUrl(); + expect(prevTileUrl).to.eql(nextTileUrl); + }); }); }); }); diff --git a/x-pack/test/functional/fixtures/kbn_archiver/maps.json b/x-pack/test/functional/fixtures/kbn_archiver/maps.json index 94ab038ae973b..5564e8d502944 100644 --- a/x-pack/test/functional/fixtures/kbn_archiver/maps.json +++ b/x-pack/test/functional/fixtures/kbn_archiver/maps.json @@ -911,6 +911,31 @@ "version": "WzY0LDJd" } +{ + "attributes": { + "description": "", + "layerListJSON": "[{\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"filterByMapBounds\":true,\"scalingType\":\"MVT\",\"id\":\"a7ab2e06-145b-48c5-bd86-b633849017ad\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"applyForceRefresh\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_0_source_index_pattern\"},\"id\":\"a7ab2e06-145b-48c5-bd86-b633849017ad\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"TILED_VECTOR\",\"joins\":[]}]", + "mapStateJSON": "{\"zoom\":3.45,\"center\":{\"lon\":-99.45039,\"lat\":41.20492},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "title": "MVT documents", + "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "coreMigrationVersion": "8.1.0", + "id": "2aff3160-3d78-11ec-9b35-f52e723e8a71", + "migrationVersion": { + "map": "8.0.0" + }, + "references": [ + { + "id": "c698b940-e149-11e8-a35a-370a8516603a", + "name": "layer_0_source_index_pattern", + "type": "index-pattern" + } + ], + "type": "map", + "updated_at": "2021-11-04T17:21:47.256Z", + "version": "Wzk1NywxXQ==" +} + { "attributes": { "description": "", From d366cffb24155e26668a4e994864583cc74225eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Mon, 8 Nov 2021 22:19:01 +0100 Subject: [PATCH 14/98] [Osquery] Fix 7.16.0 BC4 issues (#117682) --- .../osquery/common/schemas/common/schemas.ts | 2 +- .../osquery/public/actions/actions_table.tsx | 17 +-- .../public/components/osquery_icon/index.tsx | 19 ++- ...managed_policy_create_import_extension.tsx | 10 ++ .../public/live_queries/form/index.tsx | 30 ++--- .../osquery/public/packs/form/index.tsx | 2 +- .../public/packs/form/queries_field.tsx | 19 +-- .../packs/pack_queries_status_table.tsx | 50 ++++---- .../public/packs/pack_queries_table.tsx | 4 +- .../osquery/public/packs/packs_table.tsx | 2 +- .../queries/ecs_mapping_editor_field.tsx | 119 +++++++++++------- .../packs/queries/platforms/platform_icon.tsx | 19 ++- .../public/packs/use_pack_query_errors.ts | 10 +- .../packs/use_pack_query_last_results.ts | 13 +- .../osquery/public/results/results_table.tsx | 89 +++++++++---- .../osquery_action/index.tsx | 2 +- .../lib/saved_query/saved_object_mappings.ts | 23 +++- x-pack/plugins/osquery/server/plugin.ts | 2 +- .../routes/action/create_action_route.ts | 17 +-- .../server/routes/pack/create_pack_route.ts | 8 +- .../server/routes/pack/update_pack_route.ts | 39 ++++-- .../saved_query/create_saved_query_route.ts | 45 ++++--- .../saved_query/update_saved_query_route.ts | 34 ++--- .../plugins/osquery/server/saved_objects.ts | 17 +-- 24 files changed, 365 insertions(+), 227 deletions(-) diff --git a/x-pack/plugins/osquery/common/schemas/common/schemas.ts b/x-pack/plugins/osquery/common/schemas/common/schemas.ts index 4547db731ce1b..24eaa11a7bf84 100644 --- a/x-pack/plugins/osquery/common/schemas/common/schemas.ts +++ b/x-pack/plugins/osquery/common/schemas/common/schemas.ts @@ -57,7 +57,7 @@ export const ecsMapping = t.record( t.string, t.partial({ field: t.string, - value: t.string, + value: t.union([t.string, t.array(t.string)]), }) ); export type ECSMapping = t.TypeOf; diff --git a/x-pack/plugins/osquery/public/actions/actions_table.tsx b/x-pack/plugins/osquery/public/actions/actions_table.tsx index 6f19979345ddf..b3464bad56340 100644 --- a/x-pack/plugins/osquery/public/actions/actions_table.tsx +++ b/x-pack/plugins/osquery/public/actions/actions_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { isArray, pickBy } from 'lodash'; +import { isArray, isEmpty, pickBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiButtonIcon, EuiCodeBlock, formatDate } from '@elastic/eui'; import React, { useState, useCallback, useMemo } from 'react'; @@ -72,12 +72,15 @@ const ActionsTableComponent = () => { const handlePlayClick = useCallback( (item) => push('/live_queries/new', { - form: pickBy({ - agentIds: item.fields.agents, - query: item._source.data.query, - ecs_mapping: item._source.data.ecs_mapping, - savedQueryId: item._source.data.saved_query_id, - }), + form: pickBy( + { + agentIds: item.fields.agents, + query: item._source.data.query, + ecs_mapping: item._source.data.ecs_mapping, + savedQueryId: item._source.data.saved_query_id, + }, + (value) => !isEmpty(value) + ), }), [push] ); diff --git a/x-pack/plugins/osquery/public/components/osquery_icon/index.tsx b/x-pack/plugins/osquery/public/components/osquery_icon/index.tsx index 0c2a24ef7b694..83e328dc7c615 100644 --- a/x-pack/plugins/osquery/public/components/osquery_icon/index.tsx +++ b/x-pack/plugins/osquery/public/components/osquery_icon/index.tsx @@ -5,14 +5,25 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiIcon, EuiIconProps } from '@elastic/eui'; import OsqueryLogo from './osquery.svg'; export type OsqueryIconProps = Omit; -const OsqueryIconComponent: React.FC = (props) => ( - -); +const OsqueryIconComponent: React.FC = (props) => { + const [Icon, setIcon] = useState(null); + + // FIXME: This is a hack to force the icon to be loaded asynchronously. + useEffect(() => { + const interval = setInterval(() => { + setIcon(); + }, 0); + + return () => clearInterval(interval); + }, [props, setIcon]); + + return Icon; +}; export const OsqueryIcon = React.memo(OsqueryIconComponent); diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index c2ac84ce191da..39975cb65ce2b 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -318,6 +318,16 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< streams: [], policy_template: 'osquery_manager', }); + } else { + if (!draft.inputs[0].type) { + set(draft, 'inputs[0].type', 'osquery'); + } + if (!draft.inputs[0].policy_template) { + set(draft, 'inputs[0].policy_template', 'osquery_manager'); + } + if (!draft.inputs[0].enabled) { + set(draft, 'inputs[0].enabled', true); + } } }); onChange({ diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 0c0151b36203c..b8a9de25ac7f8 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -86,6 +86,7 @@ const LiveQueryFormComponent: React.FC = ({ const { data, isLoading, mutateAsync, isError, isSuccess } = useMutation( (payload: Record) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any http.post('/internal/osquery/action', { body: JSON.stringify(payload), }), @@ -137,11 +138,6 @@ const LiveQueryFormComponent: React.FC = ({ type: FIELD_TYPES.JSON, validations: [], }, - hidden: { - defaultValue: false, - type: FIELD_TYPES.TOGGLE, - validations: [], - }, }; const { form } = useForm({ @@ -152,10 +148,15 @@ const LiveQueryFormComponent: React.FC = ({ if (isValid) { try { - await mutateAsync({ - ...formData, - ...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }), - }); + await mutateAsync( + pickBy( + { + ...formData, + ...(isEmpty(ecsFieldValue) ? {} : { ecs_mapping: ecsFieldValue }), + }, + (value) => !isEmpty(value) + ) + ); // eslint-disable-next-line no-empty } catch (e) {} } @@ -163,10 +164,8 @@ const LiveQueryFormComponent: React.FC = ({ options: { stripEmptyFields: false, }, - serializer: ({ savedQueryId, hidden, ...formData }) => ({ - ...pickBy({ ...formData, saved_query_id: savedQueryId }), - ...(hidden != null && hidden ? { hidden } : {}), - }), + serializer: ({ savedQueryId, ...formData }) => + pickBy({ ...formData, saved_query_id: savedQueryId }, (value) => !isEmpty(value)), defaultValue: deepMerge( { agentSelection: { @@ -177,7 +176,6 @@ const LiveQueryFormComponent: React.FC = ({ }, query: '', savedQueryId: null, - hidden: false, }, defaultValue ?? {} ), @@ -419,9 +417,6 @@ const LiveQueryFormComponent: React.FC = ({ if (defaultValue?.query) { setFieldValue('query', defaultValue?.query); } - if (defaultValue?.hidden) { - setFieldValue('hidden', defaultValue?.hidden); - } // TODO: Set query and ECS mapping from savedQueryId object if (defaultValue?.savedQueryId) { setFieldValue('savedQueryId', defaultValue?.savedQueryId); @@ -436,7 +431,6 @@ const LiveQueryFormComponent: React.FC = ({
{formType === 'steps' ? : simpleForm} - {showSavedQueryFlyout ? ( = ({ defaultValue, editMode = f defaultValue: [], type: FIELD_TYPES.COMBO_BOX, label: i18n.translate('xpack.osquery.pack.form.agentPoliciesFieldLabel', { - defaultMessage: 'Agent policies (optional)', + defaultMessage: 'Scheduled agent policies (optional)', }), helpText: i18n.translate('xpack.osquery.pack.form.agentPoliciesFieldHelpText', { defaultMessage: 'Queries in this pack are scheduled for agents in the selected policies.', diff --git a/x-pack/plugins/osquery/public/packs/form/queries_field.tsx b/x-pack/plugins/osquery/public/packs/form/queries_field.tsx index 03993bf35371c..b4b67fc8929db 100644 --- a/x-pack/plugins/osquery/public/packs/form/queries_field.tsx +++ b/x-pack/plugins/osquery/public/packs/form/queries_field.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { findIndex, forEach, pullAt, pullAllBy, pickBy } from 'lodash'; +import { isEmpty, findIndex, forEach, pullAt, pullAllBy, pickBy } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui'; import { produce } from 'immer'; import React, { useCallback, useMemo, useState } from 'react'; @@ -133,13 +133,16 @@ const QueriesFieldComponent: React.FC = ({ field, handleNameC produce((draft) => { forEach(parsedContent.queries, (newQuery, newQueryId) => { draft.push( - pickBy({ - id: newQueryId, - interval: newQuery.interval ?? parsedContent.interval, - query: newQuery.query, - version: newQuery.version ?? parsedContent.version, - platform: getSupportedPlatforms(newQuery.platform ?? parsedContent.platform), - }) + pickBy( + { + id: newQueryId, + interval: newQuery.interval ?? parsedContent.interval, + query: newQuery.query, + version: newQuery.version ?? parsedContent.version, + platform: getSupportedPlatforms(newQuery.platform ?? parsedContent.platform), + }, + (value) => !isEmpty(value) + ) ); }); diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx index 0b661c61a9057..5aacffb52cb49 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx @@ -29,7 +29,7 @@ import { PersistedIndexPatternLayer, PieVisualizationState, } from '../../../lens/public'; -import { FilterStateStore, IndexPattern } from '../../../../../src/plugins/data/common'; +import { FilterStateStore, DataView } from '../../../../../src/plugins/data/common'; import { useKibana } from '../common/lib/kibana'; import { OsqueryManagerPackagePolicyInputStream } from '../../common/types'; import { ScheduledQueryErrorsTable } from './scheduled_query_errors_table'; @@ -130,12 +130,12 @@ function getLensAttributes( references: [ { id: 'logs-*', - name: 'indexpattern-datasource-current-indexpattern', + name: 'dataView-datasource-current-dataView', type: 'index-pattern', }, { id: 'logs-*', - name: 'indexpattern-datasource-layer-layer1', + name: 'dataView-datasource-layer-layer1', type: 'index-pattern', }, { @@ -377,7 +377,7 @@ interface ScheduledQueryLastResultsProps { actionId: string; queryId: string; interval: number; - logsIndexPattern: IndexPattern | undefined; + logsDataView: DataView | undefined; toggleErrors: (payload: { queryId: string; interval: number }) => void; expanded: boolean; } @@ -386,20 +386,20 @@ const ScheduledQueryLastResults: React.FC = ({ actionId, queryId, interval, - logsIndexPattern, + logsDataView, toggleErrors, expanded, }) => { const { data: lastResultsData, isFetched } = usePackQueryLastResults({ actionId, interval, - logsIndexPattern, + logsDataView, }); const { data: errorsData, isFetched: errorsFetched } = usePackQueryErrors({ actionId, interval, - logsIndexPattern, + logsDataView, }); const handleErrorsToggle = useCallback( @@ -512,14 +512,14 @@ interface PackViewInActionProps { id: string; interval: number; }; - logsIndexPattern: IndexPattern | undefined; + logsDataView: DataView | undefined; packName: string; agentIds?: string[]; } const PackViewInDiscoverActionComponent: React.FC = ({ item, - logsIndexPattern, + logsDataView, packName, agentIds, }) => { @@ -528,7 +528,7 @@ const PackViewInDiscoverActionComponent: React.FC = ({ const { data: lastResultsData } = usePackQueryLastResults({ actionId, interval, - logsIndexPattern, + logsDataView, }); const startDate = lastResultsData?.['@timestamp'] @@ -554,7 +554,7 @@ const PackViewInDiscoverAction = React.memo(PackViewInDiscoverActionComponent); const PackViewInLensActionComponent: React.FC = ({ item, - logsIndexPattern, + logsDataView, packName, agentIds, }) => { @@ -563,7 +563,7 @@ const PackViewInLensActionComponent: React.FC = ({ const { data: lastResultsData } = usePackQueryLastResults({ actionId, interval, - logsIndexPattern, + logsDataView, }); const startDate = lastResultsData?.['@timestamp'] @@ -602,17 +602,17 @@ const PackQueriesStatusTableComponent: React.FC = ( Record> >({}); - const indexPatterns = useKibana().services.data.indexPatterns; - const [logsIndexPattern, setLogsIndexPattern] = useState(undefined); + const dataViews = useKibana().services.data.dataViews; + const [logsDataView, setLogsDataView] = useState(undefined); useEffect(() => { - const fetchLogsIndexPattern = async () => { - const indexPattern = await indexPatterns.find('logs-*'); + const fetchLogsDataView = async () => { + const dataView = await dataViews.find('logs-*'); - setLogsIndexPattern(indexPattern[0]); + setLogsDataView(dataView[0]); }; - fetchLogsIndexPattern(); - }, [indexPatterns]); + fetchLogsDataView(); + }, [dataViews]); const renderQueryColumn = useCallback( (query: string) => ( @@ -645,7 +645,7 @@ const PackQueriesStatusTableComponent: React.FC = ( const renderLastResultsColumn = useCallback( (item) => ( = ( expanded={!!itemIdToExpandedRowMap[item.id]} /> ), - [itemIdToExpandedRowMap, packName, toggleErrors, logsIndexPattern] + [itemIdToExpandedRowMap, packName, toggleErrors, logsDataView] ); const renderDiscoverResultsAction = useCallback( @@ -661,11 +661,11 @@ const PackQueriesStatusTableComponent: React.FC = ( ), - [agentIds, logsIndexPattern, packName] + [agentIds, logsDataView, packName] ); const renderLensResultsAction = useCallback( @@ -673,11 +673,11 @@ const PackQueriesStatusTableComponent: React.FC = ( ), - [agentIds, logsIndexPattern, packName] + [agentIds, logsDataView, packName] ); const getItemId = useCallback( diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_table.tsx index d23d5f6ffb06a..8a42aa9c28b72 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_table.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { PlatformIcons } from './queries/platforms'; import { OsqueryManagerPackagePolicyInputStream } from '../../common/types'; -interface PackQueriesTableProps { +export interface PackQueriesTableProps { data: OsqueryManagerPackagePolicyInputStream[]; onDeleteClick?: (item: OsqueryManagerPackagePolicyInputStream) => void; onEditClick?: (item: OsqueryManagerPackagePolicyInputStream) => void; @@ -184,3 +184,5 @@ const PackQueriesTableComponent: React.FC = ({ }; export const PackQueriesTable = React.memo(PackQueriesTableComponent); +// eslint-disable-next-line import/no-default-export +export default PackQueriesTable; diff --git a/x-pack/plugins/osquery/public/packs/packs_table.tsx b/x-pack/plugins/osquery/public/packs/packs_table.tsx index dcca0e2f56596..9bea07b7c234c 100644 --- a/x-pack/plugins/osquery/public/packs/packs_table.tsx +++ b/x-pack/plugins/osquery/public/packs/packs_table.tsx @@ -52,7 +52,7 @@ export const AgentPoliciesPopover = ({ agentPolicyIds }: { agentPolicyIds: strin const button = useMemo( () => ( - + <>{agentPolicyIds?.length ?? 0} ), diff --git a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx index 85f4b3b3f0fad..5a40be95dd824 100644 --- a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx @@ -6,7 +6,18 @@ */ import { produce } from 'immer'; -import { each, isEmpty, find, orderBy, sortedUniqBy, isArray, map, reduce, get } from 'lodash'; +import { + castArray, + each, + isEmpty, + find, + orderBy, + sortedUniqBy, + isArray, + map, + reduce, + get, +} from 'lodash'; import React, { forwardRef, useCallback, @@ -81,30 +92,24 @@ const typeMap = { }; const StyledEuiSuperSelect = styled(EuiSuperSelect)` - &.euiFormControlLayout__prepend { - padding-left: 8px; - padding-right: 24px; - box-shadow: none; - - .euiIcon { - padding: 0; - width: 18px; - background: none; - } + min-width: 70px; + border-radius: 6px 0 0 6px; + + .euiIcon { + padding: 0; + width: 18px; + background: none; } `; // @ts-expect-error update types const ResultComboBox = styled(EuiComboBox)` - &.euiComboBox--prepended .euiSuperSelect { - border-right: 1px solid ${(props) => props.theme.eui.euiBorderColor}; - - .euiFormControlLayout__childrenWrapper { - border-radius: 6px 0 0 6px; + &.euiComboBox { + position: relative; + left: -1px; - .euiFormControlLayoutIcons--right { - right: 6px; - } + .euiComboBox__inputWrap { + border-radius: 0 6px 6px 0; } } `; @@ -311,9 +316,11 @@ const OSQUERY_COLUMN_VALUE_TYPE_OPTIONS = [ }, ]; +const EMPTY_ARRAY: EuiComboBoxOptionOption[] = []; + interface OsqueryColumnFieldProps { resultType: FieldHook; - resultValue: FieldHook; + resultValue: FieldHook; euiFieldProps: EuiComboBoxProps; idAria?: string; } @@ -324,6 +331,7 @@ const OsqueryColumnFieldComponent: React.FC = ({ euiFieldProps = {}, idAria, }) => { + const inputRef = useRef(); const { setValue } = resultValue; const { setValue: setType } = resultType; const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(resultValue); @@ -367,16 +375,27 @@ const OsqueryColumnFieldComponent: React.FC = ({ (newType) => { if (newType !== resultType.value) { setType(newType); + setValue(newType === 'value' && euiFieldProps.singleSelection === false ? [] : ''); } }, - [setType, resultType.value] + [resultType.value, setType, setValue, euiFieldProps.singleSelection] ); const handleCreateOption = useCallback( - (newOption) => { - setValue(newOption); + (newOption: string) => { + if (euiFieldProps.singleSelection === false) { + setValue([newOption]); + if (resultValue.value.length) { + setValue([...castArray(resultValue.value), newOption]); + } else { + setValue([newOption]); + } + inputRef.current?.blur(); + } else { + setValue(newOption); + } }, - [setValue] + [euiFieldProps.singleSelection, resultValue.value, setValue] ); const Prepend = useMemo( @@ -400,6 +419,11 @@ const OsqueryColumnFieldComponent: React.FC = ({ setSelected(() => { if (!resultValue.value.length) return []; + // Static array values + if (isArray(resultValue.value)) { + return resultValue.value.map((value) => ({ label: value })); + } + const selectedOption = find(euiFieldProps?.options, ['label', resultValue.value]); return selectedOption ? [selectedOption] : [{ label: resultValue.value }]; @@ -416,18 +440,26 @@ const OsqueryColumnFieldComponent: React.FC = ({ describedByIds={describedByIds} isDisabled={euiFieldProps.isDisabled} > - + + {Prepend} + + { + inputRef.current = ref; + }} + fullWidth + selectedOptions={selectedOptions} + onChange={handleChange} + onCreateOption={handleCreateOption} + renderOption={renderOsqueryOption} + rowHeight={32} + isClearable + {...euiFieldProps} + options={(resultType.value === 'field' && euiFieldProps.options) || EMPTY_ARRAY} + /> + + ); }; @@ -497,7 +529,7 @@ const getOsqueryResultFieldValidator = ) => { const fieldRequiredError = fieldValidators.emptyField( i18n.translate('xpack.osquery.pack.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage', { - defaultMessage: 'Osquery result is required.', + defaultMessage: 'Value is required.', }) )(args); @@ -551,6 +583,7 @@ interface ECSMappingEditorFormRef { export const ECSMappingEditorForm = forwardRef( ({ isDisabled, osquerySchemaOptions, defaultValue, onAdd, onChange, onDelete }, ref) => { const editForm = !!defaultValue; + const multipleValuesField = useRef(false); const currentFormData = useRef(defaultValue); const formSchema = { key: { @@ -648,6 +681,8 @@ export const ECSMappingEditorForm = forwardRef )} @@ -687,17 +722,13 @@ export const ECSMappingEditorForm = forwardRef { if (!deepEqual(formData, currentFormData.current)) { currentFormData.current = formData; + const ecsOption = find(ECSSchemaOptions, ['label', formData.key]); + multipleValuesField.current = + ecsOption?.value?.normalization === 'array' && formData.result.type === 'value'; handleSubmit(); } }, [handleSubmit, formData, onAdd]); - // useEffect(() => { - // if (defaultValue) { - // validate(); - // __validateFields(['result.value']); - // } - // }, [defaultValue, osquerySchemaOptions, validate, __validateFields]); - return (
diff --git a/x-pack/plugins/osquery/public/packs/queries/platforms/platform_icon.tsx b/x-pack/plugins/osquery/public/packs/queries/platforms/platform_icon.tsx index 1126dfd690c19..62adf4558e573 100644 --- a/x-pack/plugins/osquery/public/packs/queries/platforms/platform_icon.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/platforms/platform_icon.tsx @@ -6,16 +6,27 @@ */ import { EuiIcon } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getPlatformIconModule } from './helpers'; -interface PlatformIconProps { +export interface PlatformIconProps { platform: string; } const PlatformIconComponent: React.FC = ({ platform }) => { - const platformIconModule = getPlatformIconModule(platform); - return ; + const [Icon, setIcon] = useState(null); + + // FIXME: This is a hack to force the icon to be loaded asynchronously. + useEffect(() => { + const interval = setInterval(() => { + const platformIconModule = getPlatformIconModule(platform); + setIcon(); + }, 0); + + return () => clearInterval(interval); + }, [platform, setIcon]); + + return Icon; }; export const PlatformIcon = React.memo(PlatformIconComponent); diff --git a/x-pack/plugins/osquery/public/packs/use_pack_query_errors.ts b/x-pack/plugins/osquery/public/packs/use_pack_query_errors.ts index b88bd8ce5709d..ba009b22a8d46 100644 --- a/x-pack/plugins/osquery/public/packs/use_pack_query_errors.ts +++ b/x-pack/plugins/osquery/public/packs/use_pack_query_errors.ts @@ -6,21 +6,21 @@ */ import { useQuery } from 'react-query'; -import { IndexPattern, SortDirection } from '../../../../../src/plugins/data/common'; +import { DataView, SortDirection } from '../../../../../src/plugins/data/common'; import { useKibana } from '../common/lib/kibana'; interface UsePackQueryErrorsProps { actionId: string; interval: number; - logsIndexPattern?: IndexPattern; + logsDataView?: DataView; skip?: boolean; } export const usePackQueryErrors = ({ actionId, interval, - logsIndexPattern, + logsDataView, skip = false, }: UsePackQueryErrorsProps) => { const data = useKibana().services.data; @@ -29,7 +29,7 @@ export const usePackQueryErrors = ({ ['scheduledQueryErrors', { actionId, interval }], async () => { const searchSource = await data.search.searchSource.create({ - index: logsIndexPattern, + index: logsDataView, fields: ['*'], sort: [ { @@ -73,7 +73,7 @@ export const usePackQueryErrors = ({ }, { keepPreviousData: true, - enabled: !!(!skip && actionId && interval && logsIndexPattern), + enabled: !!(!skip && actionId && interval && logsDataView), select: (response) => response.rawResponse.hits ?? [], refetchOnReconnect: false, refetchOnWindowFocus: false, diff --git a/x-pack/plugins/osquery/public/packs/use_pack_query_last_results.ts b/x-pack/plugins/osquery/public/packs/use_pack_query_last_results.ts index cb84386dbe3ea..f473ef14ccb7a 100644 --- a/x-pack/plugins/osquery/public/packs/use_pack_query_last_results.ts +++ b/x-pack/plugins/osquery/public/packs/use_pack_query_last_results.ts @@ -7,21 +7,21 @@ import { useQuery } from 'react-query'; import moment from 'moment-timezone'; -import { IndexPattern } from '../../../../../src/plugins/data/common'; +import { DataView, SortDirection } from '../../../../../src/plugins/data/common'; import { useKibana } from '../common/lib/kibana'; interface UsePackQueryLastResultsProps { actionId: string; agentIds?: string[]; interval: number; - logsIndexPattern?: IndexPattern; + logsDataView?: DataView; skip?: boolean; } export const usePackQueryLastResults = ({ actionId, interval, - logsIndexPattern, + logsDataView, skip = false, }: UsePackQueryLastResultsProps) => { const data = useKibana().services.data; @@ -30,8 +30,9 @@ export const usePackQueryLastResults = ({ ['scheduledQueryLastResults', { actionId }], async () => { const lastResultsSearchSource = await data.search.searchSource.create({ - index: logsIndexPattern, + index: logsDataView, size: 1, + sort: { '@timestamp': SortDirection.desc }, query: { // @ts-expect-error update types bool: { @@ -51,7 +52,7 @@ export const usePackQueryLastResults = ({ if (timestamp) { const aggsSearchSource = await data.search.searchSource.create({ - index: logsIndexPattern, + index: logsDataView, size: 1, aggs: { unique_agents: { cardinality: { field: 'agent.id' } }, @@ -92,7 +93,7 @@ export const usePackQueryLastResults = ({ }, { keepPreviousData: true, - enabled: !!(!skip && actionId && interval && logsIndexPattern), + enabled: !!(!skip && actionId && interval && logsDataView), refetchOnReconnect: false, refetchOnWindowFocus: false, } diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index d1d16730e7982..164d4fbdc878b 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { get, isEmpty, isEqual, keys, map, reduce } from 'lodash/fp'; +import { get, isEmpty, isArray, isObject, isEqual, keys, map, reduce } from 'lodash/fp'; import { EuiCallOut, EuiCode, @@ -123,6 +123,11 @@ const ResultsTableComponent: React.FC = ({ [visibleColumns, setVisibleColumns] ); + const ecsMappingColumns = useMemo( + () => keys(get('actionDetails._source.data.ecs_mapping', actionDetails) || {}), + [actionDetails] + ); + const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( () => // eslint-disable-next-line react/display-name @@ -140,9 +145,22 @@ const ResultsTableComponent: React.FC = ({ return {value}; } + if (ecsMappingColumns.includes(columnId)) { + const ecsFieldValue = get(columnId, data[rowIndex % pagination.pageSize]?._source); + + if (isArray(ecsFieldValue) || isObject(ecsFieldValue)) { + try { + return JSON.stringify(ecsFieldValue, null, 2); + // eslint-disable-next-line no-empty + } catch (e) {} + } + + return ecsFieldValue ?? '-'; + } + return !isEmpty(value) ? value : '-'; }, - [getFleetAppUrl, pagination.pageSize] + [ecsMappingColumns, getFleetAppUrl, pagination.pageSize] ); const tableSorting = useMemo( @@ -218,12 +236,17 @@ const ResultsTableComponent: React.FC = ({ return; } - const newColumns = keys(allResultsData?.edges[0]?.fields) - .sort() - .reduce( - (acc, fieldName) => { - const { data, seen } = acc; - if (fieldName === 'agent.name') { + const fields = [ + 'agent.name', + ...ecsMappingColumns.sort(), + ...keys(allResultsData?.edges[0]?.fields || {}).sort(), + ]; + + const newColumns = fields.reduce( + (acc, fieldName) => { + const { data, seen } = acc; + if (fieldName === 'agent.name') { + if (!seen.has(fieldName)) { data.push({ id: fieldName, displayAsText: i18n.translate( @@ -234,34 +257,48 @@ const ResultsTableComponent: React.FC = ({ ), defaultSortDirection: Direction.asc, }); - - return acc; + seen.add(fieldName); } - if (fieldName.startsWith('osquery.')) { - const displayAsText = fieldName.split('.')[1]; - if (!seen.has(displayAsText)) { - data.push({ - id: fieldName, - displayAsText, - display: getHeaderDisplay(displayAsText), - defaultSortDirection: Direction.asc, - }); - seen.add(displayAsText); - } - return acc; + return acc; + } + + if (ecsMappingColumns.includes(fieldName)) { + if (!seen.has(fieldName)) { + data.push({ + id: fieldName, + displayAsText: fieldName, + defaultSortDirection: Direction.asc, + }); + seen.add(fieldName); } + return acc; + } + if (fieldName.startsWith('osquery.')) { + const displayAsText = fieldName.split('.')[1]; + if (!seen.has(displayAsText)) { + data.push({ + id: fieldName, + displayAsText, + display: getHeaderDisplay(displayAsText), + defaultSortDirection: Direction.asc, + }); + seen.add(displayAsText); + } return acc; - }, - { data: [], seen: new Set() } as { data: EuiDataGridColumn[]; seen: Set } - ).data; + } + + return acc; + }, + { data: [], seen: new Set() } as { data: EuiDataGridColumn[]; seen: Set } + ).data; setColumns((currentColumns) => !isEqual(map('id', currentColumns), map('id', newColumns)) ? newColumns : currentColumns ); setVisibleColumns(map('id', newColumns)); - }, [allResultsData?.edges, getHeaderDisplay]); + }, [allResultsData?.edges, ecsMappingColumns, getHeaderDisplay]); const toolbarVisibility = useMemo( () => ({ diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx index 53b8ea436c124..8fc289b7ef36b 100644 --- a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx @@ -127,7 +127,7 @@ const OsqueryActionComponent: React.FC = ({ metadata }) => { ); } - return ; + return ; }; export const OsqueryAction = React.memo(OsqueryActionComponent); diff --git a/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts b/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts index fb2c834f3c74d..2990027ff8d97 100644 --- a/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts +++ b/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { produce } from 'immer'; import { SavedObjectsType } from '../../../../../../src/core/server'; - import { savedQuerySavedObjectType, packSavedObjectType } from '../../../common/types'; export const savedQuerySavedObjectMappings: SavedObjectsType['mappings'] = { @@ -54,9 +54,13 @@ export const savedQueryType: SavedObjectsType = { namespaceType: 'multiple-isolated', mappings: savedQuerySavedObjectMappings, management: { - defaultSearchField: 'id', importableAndExportable: true, getTitle: (savedObject) => savedObject.attributes.id, + getEditUrl: (savedObject) => `/saved_queries/${savedObject.id}/edit`, + getInAppUrl: (savedObject) => ({ + path: `/app/saved_queries/${savedObject.id}`, + uiCapabilitiesPath: 'osquery.read', + }), }, }; @@ -117,6 +121,19 @@ export const packType: SavedObjectsType = { management: { defaultSearchField: 'name', importableAndExportable: true, - getTitle: (savedObject) => savedObject.attributes.name, + getTitle: (savedObject) => `Pack: ${savedObject.attributes.name}`, + getEditUrl: (savedObject) => `/packs/${savedObject.id}/edit`, + getInAppUrl: (savedObject) => ({ + path: `/app/packs/${savedObject.id}`, + uiCapabilitiesPath: 'osquery.read', + }), + onExport: (context, objects) => + produce(objects, (draft) => { + draft.forEach((packSO) => { + packSO.references = []; + }); + + return draft; + }), }, }; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 1bb394843e5b7..16f0b17cc10df 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -227,7 +227,7 @@ export class OsqueryPlugin implements Plugin !isEmpty(value) + ), }; const actionResponse = await esClient.index<{}, {}>({ index: '.fleet-actions', diff --git a/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts b/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts index 16710d578abb7..bdc307e36619f 100644 --- a/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts +++ b/x-pack/plugins/osquery/server/routes/pack/create_pack_route.ts @@ -43,7 +43,10 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte schema.recordOf( schema.string(), schema.object({ - field: schema.string(), + field: schema.maybe(schema.string()), + value: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) + ), }) ) ), @@ -68,8 +71,7 @@ export const createPackRoute = (router: IRouter, osqueryContext: OsqueryAppConte const conflictingEntries = await savedObjectsClient.find({ type: packSavedObjectType, - search: name, - searchFields: ['name'], + filter: `${packSavedObjectType}.attributes.name: "${name}"`, }); if (conflictingEntries.saved_objects.length) { diff --git a/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts b/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts index 1abdec17a922b..88af904088984 100644 --- a/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts +++ b/x-pack/plugins/osquery/server/routes/pack/update_pack_route.ts @@ -6,7 +6,19 @@ */ import moment from 'moment-timezone'; -import { set, unset, has, difference, filter, find, map, mapKeys, pickBy, uniq } from 'lodash'; +import { + isEmpty, + set, + unset, + has, + difference, + filter, + find, + map, + mapKeys, + pickBy, + uniq, +} from 'lodash'; import { schema } from '@kbn/config-schema'; import { produce } from 'immer'; import { @@ -51,7 +63,10 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte schema.recordOf( schema.string(), schema.object({ - field: schema.string(), + field: schema.maybe(schema.string()), + value: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) + ), }) ) ), @@ -82,8 +97,7 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte if (name) { const conflictingEntries = await savedObjectsClient.find({ type: packSavedObjectType, - search: name, - searchFields: ['name'], + filter: `${packSavedObjectType}.attributes.name: "${name}"`, }); if ( @@ -112,13 +126,16 @@ export const updatePackRoute = (router: IRouter, osqueryContext: OsqueryAppConte request.params.id, { enabled, - ...pickBy({ - name, - description, - queries: queries && convertPackQueriesToSO(queries), - updated_at: moment().toISOString(), - updated_by: currentUser, - }), + ...pickBy( + { + name, + description, + queries: queries && convertPackQueriesToSO(queries), + updated_at: moment().toISOString(), + updated_by: currentUser, + }, + (value) => !isEmpty(value) + ), }, policy_ids ? { diff --git a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts index 5c65ebf1a701e..66c114012fd78 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { pickBy } from 'lodash'; +import { isEmpty, pickBy } from 'lodash'; import { IRouter } from '../../../../../../src/core/server'; import { PLUGIN_ID } from '../../../common'; import { @@ -39,8 +39,7 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const conflictingEntries = await savedObjectsClient.find({ type: savedQuerySavedObjectType, - search: id, - searchFields: ['id'], + filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`, }); if (conflictingEntries.saved_objects.length) { @@ -49,26 +48,32 @@ export const createSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const savedQuerySO = await savedObjectsClient.create( savedQuerySavedObjectType, - pickBy({ - id, - description, - query, - platform, - version, - interval, - ecs_mapping: convertECSMappingToArray(ecs_mapping), - created_by: currentUser, - created_at: new Date().toISOString(), - updated_by: currentUser, - updated_at: new Date().toISOString(), - }) + pickBy( + { + id, + description, + query, + platform, + version, + interval, + ecs_mapping: convertECSMappingToArray(ecs_mapping), + created_by: currentUser, + created_at: new Date().toISOString(), + updated_by: currentUser, + updated_at: new Date().toISOString(), + }, + (value) => !isEmpty(value) + ) ); return response.ok({ - body: pickBy({ - ...savedQuerySO, - ecs_mapping, - }), + body: pickBy( + { + ...savedQuerySO, + ecs_mapping, + }, + (value) => !isEmpty(value) + ), }); } ); diff --git a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts index b34999204b8a3..21cfd0bd43772 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { filter, pickBy } from 'lodash'; +import { isEmpty, filter, pickBy } from 'lodash'; import { schema } from '@kbn/config-schema'; import { PLUGIN_ID } from '../../../common'; @@ -35,7 +35,9 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp schema.string(), schema.object({ field: schema.maybe(schema.string()), - value: schema.maybe(schema.string()), + value: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) + ), }) ) ), @@ -62,8 +64,7 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const conflictingEntries = await savedObjectsClient.find<{ id: string }>({ type: savedQuerySavedObjectType, - search: id, - searchFields: ['id'], + filter: `${savedQuerySavedObjectType}.attributes.id: "${id}"`, }); if ( @@ -76,17 +77,20 @@ export const updateSavedQueryRoute = (router: IRouter, osqueryContext: OsqueryAp const updatedSavedQuerySO = await savedObjectsClient.update( savedQuerySavedObjectType, request.params.id, - pickBy({ - id, - description, - platform, - query, - version, - interval, - ecs_mapping: convertECSMappingToArray(ecs_mapping), - updated_by: currentUser, - updated_at: new Date().toISOString(), - }), + pickBy( + { + id, + description, + platform, + query, + version, + interval, + ecs_mapping: convertECSMappingToArray(ecs_mapping), + updated_by: currentUser, + updated_at: new Date().toISOString(), + }, + (value) => !isEmpty(value) + ), { refresh: 'wait_for', } diff --git a/x-pack/plugins/osquery/server/saved_objects.ts b/x-pack/plugins/osquery/server/saved_objects.ts index 27e7dd28ce664..16a1f2efb7e9d 100644 --- a/x-pack/plugins/osquery/server/saved_objects.ts +++ b/x-pack/plugins/osquery/server/saved_objects.ts @@ -7,24 +7,11 @@ import { CoreSetup } from '../../../../src/core/server'; -import { OsqueryAppContext } from './lib/osquery_app_context_services'; import { savedQueryType, packType } from './lib/saved_query/saved_object_mappings'; import { usageMetricType } from './routes/usage/saved_object_mappings'; -const types = [savedQueryType, packType]; - -export const savedObjectTypes = types.map((type) => type.name); - -export const initSavedObjects = ( - savedObjects: CoreSetup['savedObjects'], - osqueryContext: OsqueryAppContext -) => { - const config = osqueryContext.config(); - +export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => { savedObjects.registerType(usageMetricType); savedObjects.registerType(savedQueryType); - - if (config.packs) { - savedObjects.registerType(packType); - } + savedObjects.registerType(packType); }; From 62f7c9da0f7df51aef3fe1db6c837591c33cf47f Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 8 Nov 2021 15:47:52 -0700 Subject: [PATCH 15/98] Better support M1 users (#117766) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 3 +- packages/kbn-pm/dist/index.js | 1435 +++++++++-------- packages/kbn-pm/src/commands/index.ts | 2 + .../src/commands/patch_native_modules.ts | 68 + 4 files changed, 831 insertions(+), 677 deletions(-) create mode 100644 packages/kbn-pm/src/commands/patch_native_modules.ts diff --git a/package.json b/package.json index 467ce7f60e0eb..8c29c8bcf2ff6 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,8 @@ "backport-skip-ci": "backport --prDescription \"[skip-ci]\"", "storybook": "node scripts/storybook", "cover:report": "nyc report --temp-dir target/kibana-coverage/functional --report-dir target/coverage/report --reporter=lcov && open ./target/coverage/report/lcov-report/index.html", - "cover:functional:merge": "nyc report --temp-dir target/kibana-coverage/functional --report-dir target/coverage/report/functional --reporter=json-summary" + "cover:functional:merge": "nyc report --temp-dir target/kibana-coverage/functional --report-dir target/coverage/report/functional --reporter=json-summary", + "postinstall": "node scripts/kbn patch_native_modules" }, "repository": { "type": "git", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index d2c067759e25f..49729eee0aa1e 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(560); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(561); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildBazelProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); @@ -108,7 +108,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(349); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return _utils_package_json__WEBPACK_IMPORTED_MODULE_4__["transformDependencies"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(559); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(560); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -141,7 +141,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(129); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(554); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(555); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(220); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one @@ -8816,6 +8816,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _reset__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(550); /* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(551); /* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(553); +/* harmony import */ var _patch_native_modules__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(554); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -8829,13 +8830,15 @@ __webpack_require__.r(__webpack_exports__); + const commands = { bootstrap: _bootstrap__WEBPACK_IMPORTED_MODULE_0__["BootstrapCommand"], build: _build__WEBPACK_IMPORTED_MODULE_1__["BuildCommand"], clean: _clean__WEBPACK_IMPORTED_MODULE_2__["CleanCommand"], reset: _reset__WEBPACK_IMPORTED_MODULE_3__["ResetCommand"], run: _run__WEBPACK_IMPORTED_MODULE_4__["RunCommand"], - watch: _watch__WEBPACK_IMPORTED_MODULE_5__["WatchCommand"] + watch: _watch__WEBPACK_IMPORTED_MODULE_5__["WatchCommand"], + patch_native_modules: _patch_native_modules__WEBPACK_IMPORTED_MODULE_6__["PatchNativeModulesCommand"] }; /***/ }), @@ -62929,6 +62932,86 @@ const WatchCommand = { /* 554 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "PatchNativeModulesCommand", function() { return PatchNativeModulesCommand; }); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(132); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(131); +/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(220); +/* harmony import */ var _utils_child_process__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(221); +/* + * 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. + */ + + + + + +const PatchNativeModulesCommand = { + description: 'Patch native modules by running build commands on M1 Macs', + name: 'patch_native_modules', + + async run(projects, _, { + kbn + }) { + var _projects$get; + + const kibanaProjectPath = ((_projects$get = projects.get('kibana')) === null || _projects$get === void 0 ? void 0 : _projects$get.path) || ''; + const reporter = _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_2__["CiStatsReporter"].fromEnv(_utils_log__WEBPACK_IMPORTED_MODULE_3__["log"]); + + if (process.platform !== 'darwin' || process.arch !== 'arm64') { + return; + } + + const startTime = Date.now(); + const nodeSassDir = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(kibanaProjectPath, 'node_modules/node-sass'); + const nodeSassNativeDist = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(nodeSassDir, `vendor/darwin-arm64-${process.versions.modules}/binding.node`); + + if (!fs__WEBPACK_IMPORTED_MODULE_1___default.a.existsSync(nodeSassNativeDist)) { + _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].info('Running build script for node-sass'); + await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_4__["spawn"])('npm', ['run', 'build'], { + cwd: nodeSassDir + }); + } + + const re2Dir = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(kibanaProjectPath, 'node_modules/re2'); + const re2NativeDist = path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(re2Dir, 'build/Release/re2.node'); + + if (!fs__WEBPACK_IMPORTED_MODULE_1___default.a.existsSync(re2NativeDist)) { + _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].info('Running build script for re2'); + await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_4__["spawn"])('npm', ['run', 'rebuild'], { + cwd: re2Dir + }); + } + + _utils_log__WEBPACK_IMPORTED_MODULE_3__["log"].success('native modules should be setup for native ARM Mac development'); // send timings + + await reporter.timings({ + upstreamBranch: kbn.kibanaProject.json.branch, + // prevent loading @kbn/utils by passing null + kibanaUuid: kbn.getUuid() || null, + timings: [{ + group: 'scripts/kbn bootstrap', + id: 'patch native modudles for arm macs', + ms: Date.now() - startTime + }] + }); + } + +}; + +/***/ }), +/* 555 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); @@ -62938,7 +63021,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(220); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(346); /* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(418); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(555); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(556); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -63057,7 +63140,7 @@ function toArray(value) { } /***/ }), -/* 555 */ +/* 556 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -63067,13 +63150,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(132); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(556); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(557); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(339); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(414); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(346); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(559); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(560); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -63237,15 +63320,15 @@ class Kibana { } /***/ }), -/* 556 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(247); const arrayUnion = __webpack_require__(242); -const arrayDiffer = __webpack_require__(557); -const arrify = __webpack_require__(558); +const arrayDiffer = __webpack_require__(558); +const arrify = __webpack_require__(559); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -63269,7 +63352,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 557 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63284,7 +63367,7 @@ module.exports = arrayDiffer; /***/ }), -/* 558 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63314,7 +63397,7 @@ module.exports = arrify; /***/ }), -/* 559 */ +/* 560 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -63374,15 +63457,15 @@ function getProjectPaths({ } /***/ }), -/* 560 */ +/* 561 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(561); +/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(808); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(809); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); /* @@ -63396,19 +63479,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 561 */ +/* 562 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return buildBazelProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(563); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(774); +/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(775); /* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(808); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(809); /* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(419); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(231); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(220); @@ -63503,7 +63586,7 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { } /***/ }), -/* 562 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63511,14 +63594,14 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(164); const path = __webpack_require__(4); const os = __webpack_require__(122); -const pMap = __webpack_require__(563); -const arrify = __webpack_require__(558); -const globby = __webpack_require__(566); -const hasGlob = __webpack_require__(758); -const cpFile = __webpack_require__(760); -const junk = __webpack_require__(770); -const pFilter = __webpack_require__(771); -const CpyError = __webpack_require__(773); +const pMap = __webpack_require__(564); +const arrify = __webpack_require__(559); +const globby = __webpack_require__(567); +const hasGlob = __webpack_require__(759); +const cpFile = __webpack_require__(761); +const junk = __webpack_require__(771); +const pFilter = __webpack_require__(772); +const CpyError = __webpack_require__(774); const defaultOptions = { ignoreJunk: true @@ -63669,12 +63752,12 @@ module.exports = (source, destination, { /***/ }), -/* 563 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(564); +const AggregateError = __webpack_require__(565); module.exports = async ( iterable, @@ -63757,12 +63840,12 @@ module.exports = async ( /***/ }), -/* 564 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(565); +const indentString = __webpack_require__(566); const cleanStack = __webpack_require__(344); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -63811,7 +63894,7 @@ module.exports = AggregateError; /***/ }), -/* 565 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63853,17 +63936,17 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 566 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const arrayUnion = __webpack_require__(567); +const arrayUnion = __webpack_require__(568); const glob = __webpack_require__(244); -const fastGlob = __webpack_require__(569); -const dirGlob = __webpack_require__(752); -const gitignore = __webpack_require__(755); +const fastGlob = __webpack_require__(570); +const dirGlob = __webpack_require__(753); +const gitignore = __webpack_require__(756); const DEFAULT_FILTER = () => false; @@ -64008,12 +64091,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 567 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(568); +var arrayUniq = __webpack_require__(569); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -64021,7 +64104,7 @@ module.exports = function () { /***/ }), -/* 568 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64090,10 +64173,10 @@ if ('Set' in global) { /***/ }), -/* 569 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(570); +const pkg = __webpack_require__(571); module.exports = pkg.async; module.exports.default = pkg.async; @@ -64106,19 +64189,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 570 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(571); -var taskManager = __webpack_require__(572); -var reader_async_1 = __webpack_require__(723); -var reader_stream_1 = __webpack_require__(747); -var reader_sync_1 = __webpack_require__(748); -var arrayUtils = __webpack_require__(750); -var streamUtils = __webpack_require__(751); +var optionsManager = __webpack_require__(572); +var taskManager = __webpack_require__(573); +var reader_async_1 = __webpack_require__(724); +var reader_stream_1 = __webpack_require__(748); +var reader_sync_1 = __webpack_require__(749); +var arrayUtils = __webpack_require__(751); +var streamUtils = __webpack_require__(752); /** * Synchronous API. */ @@ -64184,7 +64267,7 @@ function isString(source) { /***/ }), -/* 571 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64222,13 +64305,13 @@ exports.prepare = prepare; /***/ }), -/* 572 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(573); +var patternUtils = __webpack_require__(574); /** * Generate tasks based on parent directory of each pattern. */ @@ -64319,16 +64402,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 573 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(574); +var globParent = __webpack_require__(575); var isGlob = __webpack_require__(266); -var micromatch = __webpack_require__(577); +var micromatch = __webpack_require__(578); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -64474,15 +64557,15 @@ exports.matchAny = matchAny; /***/ }), -/* 574 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(575); -var pathDirname = __webpack_require__(576); +var isglob = __webpack_require__(576); +var pathDirname = __webpack_require__(577); var isWin32 = __webpack_require__(122).platform() === 'win32'; module.exports = function globParent(str) { @@ -64505,7 +64588,7 @@ module.exports = function globParent(str) { /***/ }), -/* 575 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -64536,7 +64619,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 576 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64686,7 +64769,7 @@ module.exports.win32 = win32; /***/ }), -/* 577 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64697,18 +64780,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(113); -var braces = __webpack_require__(578); -var toRegex = __webpack_require__(579); -var extend = __webpack_require__(691); +var braces = __webpack_require__(579); +var toRegex = __webpack_require__(580); +var extend = __webpack_require__(692); /** * Local dependencies */ -var compilers = __webpack_require__(693); -var parsers = __webpack_require__(719); -var cache = __webpack_require__(720); -var utils = __webpack_require__(721); +var compilers = __webpack_require__(694); +var parsers = __webpack_require__(720); +var cache = __webpack_require__(721); +var utils = __webpack_require__(722); var MAX_LENGTH = 1024 * 64; /** @@ -65570,7 +65653,7 @@ module.exports = micromatch; /***/ }), -/* 578 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65580,18 +65663,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(579); -var unique = __webpack_require__(599); -var extend = __webpack_require__(600); +var toRegex = __webpack_require__(580); +var unique = __webpack_require__(600); +var extend = __webpack_require__(601); /** * Local dependencies */ -var compilers = __webpack_require__(602); -var parsers = __webpack_require__(617); -var Braces = __webpack_require__(622); -var utils = __webpack_require__(603); +var compilers = __webpack_require__(603); +var parsers = __webpack_require__(618); +var Braces = __webpack_require__(623); +var utils = __webpack_require__(604); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -65895,16 +65978,16 @@ module.exports = braces; /***/ }), -/* 579 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(580); -var define = __webpack_require__(586); -var extend = __webpack_require__(592); -var not = __webpack_require__(596); +var safe = __webpack_require__(581); +var define = __webpack_require__(587); +var extend = __webpack_require__(593); +var not = __webpack_require__(597); var MAX_LENGTH = 1024 * 64; /** @@ -66057,10 +66140,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 580 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(581); +var parse = __webpack_require__(582); var types = parse.types; module.exports = function (re, opts) { @@ -66106,13 +66189,13 @@ function isRegExp (x) { /***/ }), -/* 581 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(582); -var types = __webpack_require__(583); -var sets = __webpack_require__(584); -var positions = __webpack_require__(585); +var util = __webpack_require__(583); +var types = __webpack_require__(584); +var sets = __webpack_require__(585); +var positions = __webpack_require__(586); module.exports = function(regexpStr) { @@ -66394,11 +66477,11 @@ module.exports.types = types; /***/ }), -/* 582 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(583); -var sets = __webpack_require__(584); +var types = __webpack_require__(584); +var sets = __webpack_require__(585); // All of these are private and only used by randexp. @@ -66511,7 +66594,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 583 */ +/* 584 */ /***/ (function(module, exports) { module.exports = { @@ -66527,10 +66610,10 @@ module.exports = { /***/ }), -/* 584 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(583); +var types = __webpack_require__(584); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -66615,10 +66698,10 @@ exports.anyChar = function() { /***/ }), -/* 585 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(583); +var types = __webpack_require__(584); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -66638,7 +66721,7 @@ exports.end = function() { /***/ }), -/* 586 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66651,8 +66734,8 @@ exports.end = function() { -var isobject = __webpack_require__(587); -var isDescriptor = __webpack_require__(588); +var isobject = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -66683,7 +66766,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 587 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66702,7 +66785,7 @@ module.exports = function isObject(val) { /***/ }), -/* 588 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66715,9 +66798,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(589); -var isAccessor = __webpack_require__(590); -var isData = __webpack_require__(591); +var typeOf = __webpack_require__(590); +var isAccessor = __webpack_require__(591); +var isData = __webpack_require__(592); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -66731,7 +66814,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 589 */ +/* 590 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -66866,7 +66949,7 @@ function isBuffer(val) { /***/ }), -/* 590 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66879,7 +66962,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(589); +var typeOf = __webpack_require__(590); // accessor descriptor properties var accessor = { @@ -66942,7 +67025,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 591 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66955,7 +67038,7 @@ module.exports = isAccessorDescriptor; -var typeOf = __webpack_require__(589); +var typeOf = __webpack_require__(590); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -66998,14 +67081,14 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 592 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(593); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(594); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67065,7 +67148,7 @@ function isEnum(obj, key) { /***/ }), -/* 593 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67078,7 +67161,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67086,7 +67169,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 594 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67099,7 +67182,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(587); +var isObject = __webpack_require__(588); function isObjectObject(o) { return isObject(o) === true @@ -67130,7 +67213,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 595 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67177,14 +67260,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 596 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(597); -var safe = __webpack_require__(580); +var extend = __webpack_require__(598); +var safe = __webpack_require__(581); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -67256,14 +67339,14 @@ module.exports = toRegex; /***/ }), -/* 597 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(598); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(599); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67323,7 +67406,7 @@ function isEnum(obj, key) { /***/ }), -/* 598 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67336,7 +67419,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67344,7 +67427,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 599 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67394,13 +67477,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 600 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(601); +var isObject = __webpack_require__(602); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -67434,7 +67517,7 @@ function hasOwn(obj, key) { /***/ }), -/* 601 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67454,13 +67537,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 602 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(603); +var utils = __webpack_require__(604); module.exports = function(braces, options) { braces.compiler @@ -67743,25 +67826,25 @@ function hasQueue(node) { /***/ }), -/* 603 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(604); +var splitString = __webpack_require__(605); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(600); -utils.flatten = __webpack_require__(607); -utils.isObject = __webpack_require__(587); -utils.fillRange = __webpack_require__(608); -utils.repeat = __webpack_require__(616); -utils.unique = __webpack_require__(599); +utils.extend = __webpack_require__(601); +utils.flatten = __webpack_require__(608); +utils.isObject = __webpack_require__(588); +utils.fillRange = __webpack_require__(609); +utils.repeat = __webpack_require__(617); +utils.unique = __webpack_require__(600); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -68093,7 +68176,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 604 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68106,7 +68189,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(605); +var extend = __webpack_require__(606); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -68271,14 +68354,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 605 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(606); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(607); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -68338,7 +68421,7 @@ function isEnum(obj, key) { /***/ }), -/* 606 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68351,7 +68434,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -68359,7 +68442,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 607 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68388,7 +68471,7 @@ function flat(arr, res) { /***/ }), -/* 608 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68402,10 +68485,10 @@ function flat(arr, res) { var util = __webpack_require__(113); -var isNumber = __webpack_require__(609); -var extend = __webpack_require__(612); -var repeat = __webpack_require__(614); -var toRegex = __webpack_require__(615); +var isNumber = __webpack_require__(610); +var extend = __webpack_require__(613); +var repeat = __webpack_require__(615); +var toRegex = __webpack_require__(616); /** * Return a range of numbers or letters. @@ -68603,7 +68686,7 @@ module.exports = fillRange; /***/ }), -/* 609 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68616,7 +68699,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(610); +var typeOf = __webpack_require__(611); module.exports = function isNumber(num) { var type = typeOf(num); @@ -68632,10 +68715,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 610 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -68754,7 +68837,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 611 */ +/* 612 */ /***/ (function(module, exports) { /*! @@ -68781,13 +68864,13 @@ function isSlowBuffer (obj) { /***/ }), -/* 612 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(613); +var isObject = __webpack_require__(614); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -68821,7 +68904,7 @@ function hasOwn(obj, key) { /***/ }), -/* 613 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68841,7 +68924,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 614 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68918,7 +69001,7 @@ function repeat(str, num) { /***/ }), -/* 615 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68931,8 +69014,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(614); -var isNumber = __webpack_require__(609); +var repeat = __webpack_require__(615); +var isNumber = __webpack_require__(610); var cache = {}; function toRegexRange(min, max, options) { @@ -69219,7 +69302,7 @@ module.exports = toRegexRange; /***/ }), -/* 616 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69244,14 +69327,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 617 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(618); -var utils = __webpack_require__(603); +var Node = __webpack_require__(619); +var utils = __webpack_require__(604); /** * Braces parsers @@ -69611,15 +69694,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 618 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(587); -var define = __webpack_require__(619); -var utils = __webpack_require__(620); +var isObject = __webpack_require__(588); +var define = __webpack_require__(620); +var utils = __webpack_require__(621); var ownNames; /** @@ -70110,7 +70193,7 @@ exports = module.exports = Node; /***/ }), -/* 619 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70123,7 +70206,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -70148,13 +70231,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 620 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(621); +var typeOf = __webpack_require__(622); var utils = module.exports; /** @@ -71174,10 +71257,10 @@ function assert(val, message) { /***/ }), -/* 621 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -71296,17 +71379,17 @@ module.exports = function kindOf(val) { /***/ }), -/* 622 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(600); -var Snapdragon = __webpack_require__(623); -var compilers = __webpack_require__(602); -var parsers = __webpack_require__(617); -var utils = __webpack_require__(603); +var extend = __webpack_require__(601); +var Snapdragon = __webpack_require__(624); +var compilers = __webpack_require__(603); +var parsers = __webpack_require__(618); +var utils = __webpack_require__(604); /** * Customize Snapdragon parser and renderer @@ -71407,17 +71490,17 @@ module.exports = Braces; /***/ }), -/* 623 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(624); -var define = __webpack_require__(654); -var Compiler = __webpack_require__(665); -var Parser = __webpack_require__(688); -var utils = __webpack_require__(668); +var Base = __webpack_require__(625); +var define = __webpack_require__(655); +var Compiler = __webpack_require__(666); +var Parser = __webpack_require__(689); +var utils = __webpack_require__(669); var regexCache = {}; var cache = {}; @@ -71588,20 +71671,20 @@ module.exports.Parser = Parser; /***/ }), -/* 624 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var define = __webpack_require__(625); -var CacheBase = __webpack_require__(626); -var Emitter = __webpack_require__(627); -var isObject = __webpack_require__(587); -var merge = __webpack_require__(648); -var pascal = __webpack_require__(651); -var cu = __webpack_require__(652); +var define = __webpack_require__(626); +var CacheBase = __webpack_require__(627); +var Emitter = __webpack_require__(628); +var isObject = __webpack_require__(588); +var merge = __webpack_require__(649); +var pascal = __webpack_require__(652); +var cu = __webpack_require__(653); /** * Optionally define a custom `cache` namespace to use. @@ -72030,7 +72113,7 @@ module.exports.namespace = namespace; /***/ }), -/* 625 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72043,7 +72126,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -72068,21 +72151,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 626 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(587); -var Emitter = __webpack_require__(627); -var visit = __webpack_require__(628); -var toPath = __webpack_require__(631); -var union = __webpack_require__(633); -var del = __webpack_require__(639); -var get = __webpack_require__(636); -var has = __webpack_require__(644); -var set = __webpack_require__(647); +var isObject = __webpack_require__(588); +var Emitter = __webpack_require__(628); +var visit = __webpack_require__(629); +var toPath = __webpack_require__(632); +var union = __webpack_require__(634); +var del = __webpack_require__(640); +var get = __webpack_require__(637); +var has = __webpack_require__(645); +var set = __webpack_require__(648); /** * Create a `Cache` constructor that when instantiated will @@ -72336,7 +72419,7 @@ module.exports.namespace = namespace; /***/ }), -/* 627 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { @@ -72505,7 +72588,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 628 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72518,8 +72601,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(629); -var mapVisit = __webpack_require__(630); +var visit = __webpack_require__(630); +var mapVisit = __webpack_require__(631); module.exports = function(collection, method, val) { var result; @@ -72542,7 +72625,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 629 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72555,7 +72638,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(587); +var isObject = __webpack_require__(588); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -72582,14 +72665,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 630 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var visit = __webpack_require__(629); +var visit = __webpack_require__(630); /** * Map `visit` over an array of objects. @@ -72626,7 +72709,7 @@ function isObject(val) { /***/ }), -/* 631 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72639,7 +72722,7 @@ function isObject(val) { -var typeOf = __webpack_require__(632); +var typeOf = __webpack_require__(633); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -72666,10 +72749,10 @@ function filter(arr) { /***/ }), -/* 632 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -72788,16 +72871,16 @@ module.exports = function kindOf(val) { /***/ }), -/* 633 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(634); -var union = __webpack_require__(635); -var get = __webpack_require__(636); -var set = __webpack_require__(637); +var isObject = __webpack_require__(635); +var union = __webpack_require__(636); +var get = __webpack_require__(637); +var set = __webpack_require__(638); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -72825,7 +72908,7 @@ function arrayify(val) { /***/ }), -/* 634 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72845,7 +72928,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 635 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72881,7 +72964,7 @@ module.exports = function union(init) { /***/ }), -/* 636 */ +/* 637 */ /***/ (function(module, exports) { /*! @@ -72937,7 +73020,7 @@ function toString(val) { /***/ }), -/* 637 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72950,10 +73033,10 @@ function toString(val) { -var split = __webpack_require__(604); -var extend = __webpack_require__(638); -var isPlainObject = __webpack_require__(594); -var isObject = __webpack_require__(634); +var split = __webpack_require__(605); +var extend = __webpack_require__(639); +var isPlainObject = __webpack_require__(595); +var isObject = __webpack_require__(635); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -72999,13 +73082,13 @@ function isValidKey(key) { /***/ }), -/* 638 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(634); +var isObject = __webpack_require__(635); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -73039,7 +73122,7 @@ function hasOwn(obj, key) { /***/ }), -/* 639 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73052,8 +73135,8 @@ function hasOwn(obj, key) { -var isObject = __webpack_require__(587); -var has = __webpack_require__(640); +var isObject = __webpack_require__(588); +var has = __webpack_require__(641); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -73078,7 +73161,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 640 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73091,9 +73174,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(641); -var hasValues = __webpack_require__(643); -var get = __webpack_require__(636); +var isObject = __webpack_require__(642); +var hasValues = __webpack_require__(644); +var get = __webpack_require__(637); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -73104,7 +73187,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 641 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73117,7 +73200,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(642); +var isArray = __webpack_require__(643); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -73125,7 +73208,7 @@ module.exports = function isObject(val) { /***/ }), -/* 642 */ +/* 643 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -73136,7 +73219,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 643 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73179,7 +73262,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 644 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73192,9 +73275,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(587); -var hasValues = __webpack_require__(645); -var get = __webpack_require__(636); +var isObject = __webpack_require__(588); +var hasValues = __webpack_require__(646); +var get = __webpack_require__(637); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -73202,7 +73285,7 @@ module.exports = function(val, prop) { /***/ }), -/* 645 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73215,8 +73298,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(646); -var isNumber = __webpack_require__(609); +var typeOf = __webpack_require__(647); +var isNumber = __webpack_require__(610); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -73269,10 +73352,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 646 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -73394,7 +73477,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 647 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73407,10 +73490,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(604); -var extend = __webpack_require__(638); -var isPlainObject = __webpack_require__(594); -var isObject = __webpack_require__(634); +var split = __webpack_require__(605); +var extend = __webpack_require__(639); +var isPlainObject = __webpack_require__(595); +var isObject = __webpack_require__(635); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -73456,14 +73539,14 @@ function isValidKey(key) { /***/ }), -/* 648 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(649); -var forIn = __webpack_require__(650); +var isExtendable = __webpack_require__(650); +var forIn = __webpack_require__(651); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -73527,7 +73610,7 @@ module.exports = mixinDeep; /***/ }), -/* 649 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73540,7 +73623,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -73548,7 +73631,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 650 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73571,7 +73654,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 651 */ +/* 652 */ /***/ (function(module, exports) { /*! @@ -73598,14 +73681,14 @@ module.exports = pascalcase; /***/ }), -/* 652 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(113); -var utils = __webpack_require__(653); +var utils = __webpack_require__(654); /** * Expose class utils @@ -73970,7 +74053,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 653 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73984,10 +74067,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(635); -utils.define = __webpack_require__(654); -utils.isObj = __webpack_require__(587); -utils.staticExtend = __webpack_require__(661); +utils.union = __webpack_require__(636); +utils.define = __webpack_require__(655); +utils.isObj = __webpack_require__(588); +utils.staticExtend = __webpack_require__(662); /** @@ -73998,7 +74081,7 @@ module.exports = utils; /***/ }), -/* 654 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74011,7 +74094,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(655); +var isDescriptor = __webpack_require__(656); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -74036,7 +74119,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 655 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74049,9 +74132,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(656); -var isAccessor = __webpack_require__(657); -var isData = __webpack_require__(659); +var typeOf = __webpack_require__(657); +var isAccessor = __webpack_require__(658); +var isData = __webpack_require__(660); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -74065,7 +74148,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 656 */ +/* 657 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -74218,7 +74301,7 @@ function isBuffer(val) { /***/ }), -/* 657 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74231,7 +74314,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(658); +var typeOf = __webpack_require__(659); // accessor descriptor properties var accessor = { @@ -74294,10 +74377,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 658 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -74416,7 +74499,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 659 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74429,7 +74512,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(660); +var typeOf = __webpack_require__(661); // data descriptor properties var data = { @@ -74478,10 +74561,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 660 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -74600,7 +74683,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 661 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74613,8 +74696,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(662); -var define = __webpack_require__(654); +var copy = __webpack_require__(663); +var define = __webpack_require__(655); var util = __webpack_require__(113); /** @@ -74697,15 +74780,15 @@ module.exports = extend; /***/ }), -/* 662 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(663); -var copyDescriptor = __webpack_require__(664); -var define = __webpack_require__(654); +var typeOf = __webpack_require__(664); +var copyDescriptor = __webpack_require__(665); +var define = __webpack_require__(655); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -74878,10 +74961,10 @@ module.exports.has = has; /***/ }), -/* 663 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(611); +var isBuffer = __webpack_require__(612); var toString = Object.prototype.toString; /** @@ -75000,7 +75083,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 664 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75088,16 +75171,16 @@ function isObject(val) { /***/ }), -/* 665 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(666); -var define = __webpack_require__(654); +var use = __webpack_require__(667); +var define = __webpack_require__(655); var debug = __webpack_require__(205)('snapdragon:compiler'); -var utils = __webpack_require__(668); +var utils = __webpack_require__(669); /** * Create a new `Compiler` with the given `options`. @@ -75251,7 +75334,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(687); + var sourcemaps = __webpack_require__(688); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -75272,7 +75355,7 @@ module.exports = Compiler; /***/ }), -/* 666 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75285,7 +75368,7 @@ module.exports = Compiler; -var utils = __webpack_require__(667); +var utils = __webpack_require__(668); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -75400,7 +75483,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 667 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75414,8 +75497,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(654); -utils.isObject = __webpack_require__(587); +utils.define = __webpack_require__(655); +utils.isObject = __webpack_require__(588); utils.isString = function(val) { @@ -75430,7 +75513,7 @@ module.exports = utils; /***/ }), -/* 668 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75440,9 +75523,9 @@ module.exports = utils; * Module dependencies */ -exports.extend = __webpack_require__(638); -exports.SourceMap = __webpack_require__(669); -exports.sourceMapResolve = __webpack_require__(680); +exports.extend = __webpack_require__(639); +exports.SourceMap = __webpack_require__(670); +exports.sourceMapResolve = __webpack_require__(681); /** * Convert backslash in the given string to forward slashes @@ -75485,7 +75568,7 @@ exports.last = function(arr, n) { /***/ }), -/* 669 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -75493,13 +75576,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(670).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(676).SourceMapConsumer; -exports.SourceNode = __webpack_require__(679).SourceNode; +exports.SourceMapGenerator = __webpack_require__(671).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(677).SourceMapConsumer; +exports.SourceNode = __webpack_require__(680).SourceNode; /***/ }), -/* 670 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75509,10 +75592,10 @@ exports.SourceNode = __webpack_require__(679).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(671); -var util = __webpack_require__(673); -var ArraySet = __webpack_require__(674).ArraySet; -var MappingList = __webpack_require__(675).MappingList; +var base64VLQ = __webpack_require__(672); +var util = __webpack_require__(674); +var ArraySet = __webpack_require__(675).ArraySet; +var MappingList = __webpack_require__(676).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -75921,7 +76004,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 671 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75961,7 +76044,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(672); +var base64 = __webpack_require__(673); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -76067,7 +76150,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 672 */ +/* 673 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76140,7 +76223,7 @@ exports.decode = function (charCode) { /***/ }), -/* 673 */ +/* 674 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76563,7 +76646,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 674 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76573,7 +76656,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); +var util = __webpack_require__(674); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -76690,7 +76773,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 675 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76700,7 +76783,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); +var util = __webpack_require__(674); /** * Determine whether mappingB is after mappingA with respect to generated @@ -76775,7 +76858,7 @@ exports.MappingList = MappingList; /***/ }), -/* 676 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76785,11 +76868,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); -var binarySearch = __webpack_require__(677); -var ArraySet = __webpack_require__(674).ArraySet; -var base64VLQ = __webpack_require__(671); -var quickSort = __webpack_require__(678).quickSort; +var util = __webpack_require__(674); +var binarySearch = __webpack_require__(678); +var ArraySet = __webpack_require__(675).ArraySet; +var base64VLQ = __webpack_require__(672); +var quickSort = __webpack_require__(679).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -77863,7 +77946,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 677 */ +/* 678 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -77980,7 +78063,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 678 */ +/* 679 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78100,7 +78183,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 679 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78110,8 +78193,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(670).SourceMapGenerator; -var util = __webpack_require__(673); +var SourceMapGenerator = __webpack_require__(671).SourceMapGenerator; +var util = __webpack_require__(674); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -78519,17 +78602,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 680 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(681) -var resolveUrl = __webpack_require__(682) -var decodeUriComponent = __webpack_require__(683) -var urix = __webpack_require__(685) -var atob = __webpack_require__(686) +var sourceMappingURL = __webpack_require__(682) +var resolveUrl = __webpack_require__(683) +var decodeUriComponent = __webpack_require__(684) +var urix = __webpack_require__(686) +var atob = __webpack_require__(687) @@ -78827,7 +78910,7 @@ module.exports = { /***/ }), -/* 681 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -78890,7 +78973,7 @@ void (function(root, factory) { /***/ }), -/* 682 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -78908,13 +78991,13 @@ module.exports = resolveUrl /***/ }), -/* 683 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(684) +var decodeUriComponent = __webpack_require__(685) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -78925,7 +79008,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 684 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79026,7 +79109,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 685 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -79049,7 +79132,7 @@ module.exports = urix /***/ }), -/* 686 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79063,7 +79146,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 687 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79071,8 +79154,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(132); var path = __webpack_require__(4); -var define = __webpack_require__(654); -var utils = __webpack_require__(668); +var define = __webpack_require__(655); +var utils = __webpack_require__(669); /** * Expose `mixin()`. @@ -79215,19 +79298,19 @@ exports.comment = function(node) { /***/ }), -/* 688 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(666); +var use = __webpack_require__(667); var util = __webpack_require__(113); -var Cache = __webpack_require__(689); -var define = __webpack_require__(654); +var Cache = __webpack_require__(690); +var define = __webpack_require__(655); var debug = __webpack_require__(205)('snapdragon:parser'); -var Position = __webpack_require__(690); -var utils = __webpack_require__(668); +var Position = __webpack_require__(691); +var utils = __webpack_require__(669); /** * Create a new `Parser` with the given `input` and `options`. @@ -79755,7 +79838,7 @@ module.exports = Parser; /***/ }), -/* 689 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79862,13 +79945,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 690 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(654); +var define = __webpack_require__(655); /** * Store position for a node @@ -79883,14 +79966,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 691 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(692); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(693); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -79950,7 +80033,7 @@ function isEnum(obj, key) { /***/ }), -/* 692 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79963,7 +80046,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -79971,14 +80054,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 693 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(694); -var extglob = __webpack_require__(708); +var nanomatch = __webpack_require__(695); +var extglob = __webpack_require__(709); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -80055,7 +80138,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 694 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80066,17 +80149,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(113); -var toRegex = __webpack_require__(579); -var extend = __webpack_require__(695); +var toRegex = __webpack_require__(580); +var extend = __webpack_require__(696); /** * Local dependencies */ -var compilers = __webpack_require__(697); -var parsers = __webpack_require__(698); -var cache = __webpack_require__(701); -var utils = __webpack_require__(703); +var compilers = __webpack_require__(698); +var parsers = __webpack_require__(699); +var cache = __webpack_require__(702); +var utils = __webpack_require__(704); var MAX_LENGTH = 1024 * 64; /** @@ -80900,14 +80983,14 @@ module.exports = nanomatch; /***/ }), -/* 695 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(696); -var assignSymbols = __webpack_require__(595); +var isExtendable = __webpack_require__(697); +var assignSymbols = __webpack_require__(596); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -80967,7 +81050,7 @@ function isEnum(obj, key) { /***/ }), -/* 696 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80980,7 +81063,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(594); +var isPlainObject = __webpack_require__(595); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -80988,7 +81071,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 697 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81334,15 +81417,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 698 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(596); -var toRegex = __webpack_require__(579); -var isOdd = __webpack_require__(699); +var regexNot = __webpack_require__(597); +var toRegex = __webpack_require__(580); +var isOdd = __webpack_require__(700); /** * Characters to use in negation regex (we want to "not" match @@ -81728,7 +81811,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 699 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81741,7 +81824,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(700); +var isNumber = __webpack_require__(701); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -81755,7 +81838,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 700 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81783,14 +81866,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 701 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(702))(); +module.exports = new (__webpack_require__(703))(); /***/ }), -/* 702 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81803,7 +81886,7 @@ module.exports = new (__webpack_require__(702))(); -var MapCache = __webpack_require__(689); +var MapCache = __webpack_require__(690); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -81925,7 +82008,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 703 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81938,14 +82021,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(704)(); -var Snapdragon = __webpack_require__(623); -utils.define = __webpack_require__(705); -utils.diff = __webpack_require__(706); -utils.extend = __webpack_require__(695); -utils.pick = __webpack_require__(707); -utils.typeOf = __webpack_require__(589); -utils.unique = __webpack_require__(599); +var isWindows = __webpack_require__(705)(); +var Snapdragon = __webpack_require__(624); +utils.define = __webpack_require__(706); +utils.diff = __webpack_require__(707); +utils.extend = __webpack_require__(696); +utils.pick = __webpack_require__(708); +utils.typeOf = __webpack_require__(590); +utils.unique = __webpack_require__(600); /** * Returns true if the given value is effectively an empty string @@ -82311,7 +82394,7 @@ utils.unixify = function(options) { /***/ }), -/* 704 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -82339,7 +82422,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 705 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82352,8 +82435,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(587); -var isDescriptor = __webpack_require__(588); +var isobject = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -82384,7 +82467,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 706 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82438,7 +82521,7 @@ function diffArray(one, two) { /***/ }), -/* 707 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82451,7 +82534,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(587); +var isObject = __webpack_require__(588); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -82480,7 +82563,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 708 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82490,18 +82573,18 @@ module.exports = function pick(obj, keys) { * Module dependencies */ -var extend = __webpack_require__(638); -var unique = __webpack_require__(599); -var toRegex = __webpack_require__(579); +var extend = __webpack_require__(639); +var unique = __webpack_require__(600); +var toRegex = __webpack_require__(580); /** * Local dependencies */ -var compilers = __webpack_require__(709); -var parsers = __webpack_require__(715); -var Extglob = __webpack_require__(718); -var utils = __webpack_require__(717); +var compilers = __webpack_require__(710); +var parsers = __webpack_require__(716); +var Extglob = __webpack_require__(719); +var utils = __webpack_require__(718); var MAX_LENGTH = 1024 * 64; /** @@ -82818,13 +82901,13 @@ module.exports = extglob; /***/ }), -/* 709 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(710); +var brackets = __webpack_require__(711); /** * Extglob compilers @@ -82994,7 +83077,7 @@ module.exports = function(extglob) { /***/ }), -/* 710 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83004,17 +83087,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(711); -var parsers = __webpack_require__(713); +var compilers = __webpack_require__(712); +var parsers = __webpack_require__(714); /** * Module dependencies */ var debug = __webpack_require__(205)('expand-brackets'); -var extend = __webpack_require__(638); -var Snapdragon = __webpack_require__(623); -var toRegex = __webpack_require__(579); +var extend = __webpack_require__(639); +var Snapdragon = __webpack_require__(624); +var toRegex = __webpack_require__(580); /** * Parses the given POSIX character class `pattern` and returns a @@ -83212,13 +83295,13 @@ module.exports = brackets; /***/ }), -/* 711 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(712); +var posix = __webpack_require__(713); module.exports = function(brackets) { brackets.compiler @@ -83306,7 +83389,7 @@ module.exports = function(brackets) { /***/ }), -/* 712 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83335,14 +83418,14 @@ module.exports = { /***/ }), -/* 713 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(714); -var define = __webpack_require__(654); +var utils = __webpack_require__(715); +var define = __webpack_require__(655); /** * Text regex @@ -83561,14 +83644,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 714 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(579); -var regexNot = __webpack_require__(596); +var toRegex = __webpack_require__(580); +var regexNot = __webpack_require__(597); var cached; /** @@ -83602,15 +83685,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 715 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(710); -var define = __webpack_require__(716); -var utils = __webpack_require__(717); +var brackets = __webpack_require__(711); +var define = __webpack_require__(717); +var utils = __webpack_require__(718); /** * Characters to use in text regex (we want to "not" match @@ -83765,7 +83848,7 @@ module.exports = parsers; /***/ }), -/* 716 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83778,7 +83861,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83803,14 +83886,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 717 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(596); -var Cache = __webpack_require__(702); +var regex = __webpack_require__(597); +var Cache = __webpack_require__(703); /** * Utils @@ -83879,7 +83962,7 @@ utils.createRegex = function(str) { /***/ }), -/* 718 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83889,16 +83972,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(623); -var define = __webpack_require__(716); -var extend = __webpack_require__(638); +var Snapdragon = __webpack_require__(624); +var define = __webpack_require__(717); +var extend = __webpack_require__(639); /** * Local dependencies */ -var compilers = __webpack_require__(709); -var parsers = __webpack_require__(715); +var compilers = __webpack_require__(710); +var parsers = __webpack_require__(716); /** * Customize Snapdragon parser and renderer @@ -83964,16 +84047,16 @@ module.exports = Extglob; /***/ }), -/* 719 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(708); -var nanomatch = __webpack_require__(694); -var regexNot = __webpack_require__(596); -var toRegex = __webpack_require__(579); +var extglob = __webpack_require__(709); +var nanomatch = __webpack_require__(695); +var regexNot = __webpack_require__(597); +var toRegex = __webpack_require__(580); var not; /** @@ -84054,14 +84137,14 @@ function textRegex(pattern) { /***/ }), -/* 720 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(702))(); +module.exports = new (__webpack_require__(703))(); /***/ }), -/* 721 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84074,13 +84157,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(623); -utils.define = __webpack_require__(722); -utils.diff = __webpack_require__(706); -utils.extend = __webpack_require__(691); -utils.pick = __webpack_require__(707); -utils.typeOf = __webpack_require__(589); -utils.unique = __webpack_require__(599); +var Snapdragon = __webpack_require__(624); +utils.define = __webpack_require__(723); +utils.diff = __webpack_require__(707); +utils.extend = __webpack_require__(692); +utils.pick = __webpack_require__(708); +utils.typeOf = __webpack_require__(590); +utils.unique = __webpack_require__(600); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -84377,7 +84460,7 @@ utils.unixify = function(options) { /***/ }), -/* 722 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84390,8 +84473,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(587); -var isDescriptor = __webpack_require__(588); +var isobject = __webpack_require__(588); +var isDescriptor = __webpack_require__(589); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -84422,7 +84505,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 723 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84441,9 +84524,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_stream_1 = __webpack_require__(741); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_stream_1 = __webpack_require__(742); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -84504,15 +84587,15 @@ exports.default = ReaderAsync; /***/ }), -/* 724 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(725); -const readdirAsync = __webpack_require__(733); -const readdirStream = __webpack_require__(736); +const readdirSync = __webpack_require__(726); +const readdirAsync = __webpack_require__(734); +const readdirStream = __webpack_require__(737); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -84596,7 +84679,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 725 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84604,11 +84687,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(726); +const DirectoryReader = __webpack_require__(727); let syncFacade = { - fs: __webpack_require__(731), - forEach: __webpack_require__(732), + fs: __webpack_require__(732), + forEach: __webpack_require__(733), sync: true }; @@ -84637,7 +84720,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 726 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84646,9 +84729,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(173).Readable; const EventEmitter = __webpack_require__(164).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(727); -const stat = __webpack_require__(729); -const call = __webpack_require__(730); +const normalizeOptions = __webpack_require__(728); +const stat = __webpack_require__(730); +const call = __webpack_require__(731); /** * Asynchronously reads the contents of a directory and streams the results @@ -85024,14 +85107,14 @@ module.exports = DirectoryReader; /***/ }), -/* 727 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(728); +const globToRegExp = __webpack_require__(729); module.exports = normalizeOptions; @@ -85208,7 +85291,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 728 */ +/* 729 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -85345,13 +85428,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 729 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(730); +const call = __webpack_require__(731); module.exports = stat; @@ -85426,7 +85509,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 730 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85487,14 +85570,14 @@ function callOnce (fn) { /***/ }), -/* 731 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const call = __webpack_require__(730); +const call = __webpack_require__(731); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -85558,7 +85641,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 732 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85587,7 +85670,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 733 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85595,12 +85678,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(734); -const DirectoryReader = __webpack_require__(726); +const maybe = __webpack_require__(735); +const DirectoryReader = __webpack_require__(727); let asyncFacade = { fs: __webpack_require__(132), - forEach: __webpack_require__(735), + forEach: __webpack_require__(736), async: true }; @@ -85642,7 +85725,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 734 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85669,7 +85752,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 735 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85705,7 +85788,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 736 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85713,11 +85796,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(726); +const DirectoryReader = __webpack_require__(727); let streamFacade = { fs: __webpack_require__(132), - forEach: __webpack_require__(735), + forEach: __webpack_require__(736), async: true }; @@ -85737,16 +85820,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 737 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(738); -var entry_1 = __webpack_require__(740); -var pathUtil = __webpack_require__(739); +var deep_1 = __webpack_require__(739); +var entry_1 = __webpack_require__(741); +var pathUtil = __webpack_require__(740); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -85812,14 +85895,14 @@ exports.default = Reader; /***/ }), -/* 738 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(739); -var patternUtils = __webpack_require__(573); +var pathUtils = __webpack_require__(740); +var patternUtils = __webpack_require__(574); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -85902,7 +85985,7 @@ exports.default = DeepFilter; /***/ }), -/* 739 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85933,14 +86016,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 740 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(739); -var patternUtils = __webpack_require__(573); +var pathUtils = __webpack_require__(740); +var patternUtils = __webpack_require__(574); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -86025,7 +86108,7 @@ exports.default = EntryFilter; /***/ }), -/* 741 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86045,8 +86128,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(173); -var fsStat = __webpack_require__(742); -var fs_1 = __webpack_require__(746); +var fsStat = __webpack_require__(743); +var fs_1 = __webpack_require__(747); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -86096,14 +86179,14 @@ exports.default = FileSystemStream; /***/ }), -/* 742 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(743); -const statProvider = __webpack_require__(745); +const optionsManager = __webpack_require__(744); +const statProvider = __webpack_require__(746); /** * Asynchronous API. */ @@ -86134,13 +86217,13 @@ exports.statSync = statSync; /***/ }), -/* 743 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(744); +const fsAdapter = __webpack_require__(745); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -86153,7 +86236,7 @@ exports.prepare = prepare; /***/ }), -/* 744 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86176,7 +86259,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 745 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86228,7 +86311,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 746 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86259,7 +86342,7 @@ exports.default = FileSystem; /***/ }), -/* 747 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86279,9 +86362,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(173); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_stream_1 = __webpack_require__(741); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_stream_1 = __webpack_require__(742); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -86349,7 +86432,7 @@ exports.default = ReaderStream; /***/ }), -/* 748 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86368,9 +86451,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_sync_1 = __webpack_require__(749); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_sync_1 = __webpack_require__(750); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -86430,7 +86513,7 @@ exports.default = ReaderSync; /***/ }), -/* 749 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86449,8 +86532,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(742); -var fs_1 = __webpack_require__(746); +var fsStat = __webpack_require__(743); +var fs_1 = __webpack_require__(747); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -86496,7 +86579,7 @@ exports.default = FileSystemSync; /***/ }), -/* 750 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86512,7 +86595,7 @@ exports.flatten = flatten; /***/ }), -/* 751 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86533,13 +86616,13 @@ exports.merge = merge; /***/ }), -/* 752 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(753); +const pathType = __webpack_require__(754); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -86605,13 +86688,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 753 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); -const pify = __webpack_require__(754); +const pify = __webpack_require__(755); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -86654,7 +86737,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 754 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86745,17 +86828,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 755 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(132); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(569); -const gitIgnore = __webpack_require__(756); +const fastGlob = __webpack_require__(570); +const gitIgnore = __webpack_require__(757); const pify = __webpack_require__(410); -const slash = __webpack_require__(757); +const slash = __webpack_require__(758); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -86853,7 +86936,7 @@ module.exports.sync = options => { /***/ }), -/* 756 */ +/* 757 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -87322,7 +87405,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 757 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87340,7 +87423,7 @@ module.exports = input => { /***/ }), -/* 758 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87353,7 +87436,7 @@ module.exports = input => { -var isGlob = __webpack_require__(759); +var isGlob = __webpack_require__(760); module.exports = function hasGlob(val) { if (val == null) return false; @@ -87373,7 +87456,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 759 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -87404,17 +87487,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 760 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(132); -const pEvent = __webpack_require__(761); -const CpFileError = __webpack_require__(764); -const fs = __webpack_require__(766); -const ProgressEmitter = __webpack_require__(769); +const pEvent = __webpack_require__(762); +const CpFileError = __webpack_require__(765); +const fs = __webpack_require__(767); +const ProgressEmitter = __webpack_require__(770); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -87528,12 +87611,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 761 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(762); +const pTimeout = __webpack_require__(763); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -87824,12 +87907,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 762 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(763); +const pFinally = __webpack_require__(764); class TimeoutError extends Error { constructor(message) { @@ -87875,7 +87958,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 763 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87897,12 +87980,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 764 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(765); +const NestedError = __webpack_require__(766); class CpFileError extends NestedError { constructor(message, nested) { @@ -87916,7 +87999,7 @@ module.exports = CpFileError; /***/ }), -/* 765 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(113).inherits; @@ -87972,16 +88055,16 @@ module.exports = NestedError; /***/ }), -/* 766 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(113); const fs = __webpack_require__(233); -const makeDir = __webpack_require__(767); -const pEvent = __webpack_require__(761); -const CpFileError = __webpack_require__(764); +const makeDir = __webpack_require__(768); +const pEvent = __webpack_require__(762); +const CpFileError = __webpack_require__(765); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -88078,7 +88161,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 767 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88086,7 +88169,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(132); const path = __webpack_require__(4); const {promisify} = __webpack_require__(113); -const semver = __webpack_require__(768); +const semver = __webpack_require__(769); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -88241,7 +88324,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 768 */ +/* 769 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -89843,7 +89926,7 @@ function coerce (version, options) { /***/ }), -/* 769 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89884,7 +89967,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 770 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89930,12 +90013,12 @@ exports.default = module.exports; /***/ }), -/* 771 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(772); +const pMap = __webpack_require__(773); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -89952,7 +90035,7 @@ module.exports.default = pFilter; /***/ }), -/* 772 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90031,12 +90114,12 @@ module.exports.default = pMap; /***/ }), -/* 773 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(765); +const NestedError = __webpack_require__(766); class CpyError extends NestedError { constructor(message, nested) { @@ -90050,7 +90133,7 @@ module.exports = CpyError; /***/ }), -/* 774 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90058,10 +90141,10 @@ module.exports = CpyError; const fs = __webpack_require__(132); const arrayUnion = __webpack_require__(242); const merge2 = __webpack_require__(243); -const fastGlob = __webpack_require__(775); +const fastGlob = __webpack_require__(776); const dirGlob = __webpack_require__(332); -const gitignore = __webpack_require__(806); -const {FilterStream, UniqueStream} = __webpack_require__(807); +const gitignore = __webpack_require__(807); +const {FilterStream, UniqueStream} = __webpack_require__(808); const DEFAULT_FILTER = () => false; @@ -90238,17 +90321,17 @@ module.exports.gitignore = gitignore; /***/ }), -/* 775 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(776); -const async_1 = __webpack_require__(792); -const stream_1 = __webpack_require__(802); -const sync_1 = __webpack_require__(803); -const settings_1 = __webpack_require__(805); -const utils = __webpack_require__(777); +const taskManager = __webpack_require__(777); +const async_1 = __webpack_require__(793); +const stream_1 = __webpack_require__(803); +const sync_1 = __webpack_require__(804); +const settings_1 = __webpack_require__(806); +const utils = __webpack_require__(778); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -90312,14 +90395,14 @@ module.exports = FastGlob; /***/ }), -/* 776 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -90384,31 +90467,31 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 777 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; -const array = __webpack_require__(778); +const array = __webpack_require__(779); exports.array = array; -const errno = __webpack_require__(779); +const errno = __webpack_require__(780); exports.errno = errno; -const fs = __webpack_require__(780); +const fs = __webpack_require__(781); exports.fs = fs; -const path = __webpack_require__(781); +const path = __webpack_require__(782); exports.path = path; -const pattern = __webpack_require__(782); +const pattern = __webpack_require__(783); exports.pattern = pattern; -const stream = __webpack_require__(790); +const stream = __webpack_require__(791); exports.stream = stream; -const string = __webpack_require__(791); +const string = __webpack_require__(792); exports.string = string; /***/ }), -/* 778 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90437,7 +90520,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 779 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90451,7 +90534,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 780 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90477,7 +90560,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 781 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90517,7 +90600,7 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 782 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90526,7 +90609,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; const path = __webpack_require__(4); const globParent = __webpack_require__(265); -const micromatch = __webpack_require__(783); +const micromatch = __webpack_require__(784); const picomatch = __webpack_require__(285); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; @@ -90656,7 +90739,7 @@ exports.matchAny = matchAny; /***/ }), -/* 783 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90664,8 +90747,8 @@ exports.matchAny = matchAny; const util = __webpack_require__(113); const braces = __webpack_require__(269); -const picomatch = __webpack_require__(784); -const utils = __webpack_require__(787); +const picomatch = __webpack_require__(785); +const utils = __webpack_require__(788); const isEmptyString = val => val === '' || val === './'; /** @@ -91130,27 +91213,27 @@ module.exports = micromatch; /***/ }), -/* 784 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(785); +module.exports = __webpack_require__(786); /***/ }), -/* 785 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const scan = __webpack_require__(786); -const parse = __webpack_require__(789); -const utils = __webpack_require__(787); -const constants = __webpack_require__(788); +const scan = __webpack_require__(787); +const parse = __webpack_require__(790); +const utils = __webpack_require__(788); +const constants = __webpack_require__(789); const isObject = val => val && typeof val === 'object' && !Array.isArray(val); /** @@ -91489,13 +91572,13 @@ module.exports = picomatch; /***/ }), -/* 786 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(787); +const utils = __webpack_require__(788); const { CHAR_ASTERISK, /* * */ CHAR_AT, /* @ */ @@ -91512,7 +91595,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(788); +} = __webpack_require__(789); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -91887,7 +91970,7 @@ module.exports = scan; /***/ }), -/* 787 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91900,7 +91983,7 @@ const { REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL -} = __webpack_require__(788); +} = __webpack_require__(789); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -91958,7 +92041,7 @@ exports.wrapOutput = (input, state = {}, options = {}) => { /***/ }), -/* 788 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92144,14 +92227,14 @@ module.exports = { /***/ }), -/* 789 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const constants = __webpack_require__(788); -const utils = __webpack_require__(787); +const constants = __webpack_require__(789); +const utils = __webpack_require__(788); /** * Constants @@ -93235,7 +93318,7 @@ module.exports = parse; /***/ }), -/* 790 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93259,7 +93342,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 791 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93277,14 +93360,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 792 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(793); -const provider_1 = __webpack_require__(795); +const stream_1 = __webpack_require__(794); +const provider_1 = __webpack_require__(796); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -93312,7 +93395,7 @@ exports.default = ProviderAsync; /***/ }), -/* 793 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93321,7 +93404,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(173); const fsStat = __webpack_require__(295); const fsWalk = __webpack_require__(300); -const reader_1 = __webpack_require__(794); +const reader_1 = __webpack_require__(795); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -93374,7 +93457,7 @@ exports.default = ReaderStream; /***/ }), -/* 794 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93382,7 +93465,7 @@ exports.default = ReaderStream; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); const fsStat = __webpack_require__(295); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class Reader { constructor(_settings) { this._settings = _settings; @@ -93414,17 +93497,17 @@ exports.default = Reader; /***/ }), -/* 795 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(796); -const entry_1 = __webpack_require__(799); -const error_1 = __webpack_require__(800); -const entry_2 = __webpack_require__(801); +const deep_1 = __webpack_require__(797); +const entry_1 = __webpack_require__(800); +const error_1 = __webpack_require__(801); +const entry_2 = __webpack_require__(802); class Provider { constructor(_settings) { this._settings = _settings; @@ -93469,14 +93552,14 @@ exports.default = Provider; /***/ }), -/* 796 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); -const partial_1 = __webpack_require__(797); +const utils = __webpack_require__(778); +const partial_1 = __webpack_require__(798); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -93538,13 +93621,13 @@ exports.default = DeepFilter; /***/ }), -/* 797 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(798); +const matcher_1 = __webpack_require__(799); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -93583,13 +93666,13 @@ exports.default = PartialMatcher; /***/ }), -/* 798 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -93640,13 +93723,13 @@ exports.default = Matcher; /***/ }), -/* 799 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -93703,13 +93786,13 @@ exports.default = EntryFilter; /***/ }), -/* 800 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -93725,13 +93808,13 @@ exports.default = ErrorFilter; /***/ }), -/* 801 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(777); +const utils = __webpack_require__(778); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -93758,15 +93841,15 @@ exports.default = EntryTransformer; /***/ }), -/* 802 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(173); -const stream_2 = __webpack_require__(793); -const provider_1 = __webpack_require__(795); +const stream_2 = __webpack_require__(794); +const provider_1 = __webpack_require__(796); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -93796,14 +93879,14 @@ exports.default = ProviderStream; /***/ }), -/* 803 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(804); -const provider_1 = __webpack_require__(795); +const sync_1 = __webpack_require__(805); +const provider_1 = __webpack_require__(796); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -93826,7 +93909,7 @@ exports.default = ProviderSync; /***/ }), -/* 804 */ +/* 805 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93834,7 +93917,7 @@ exports.default = ProviderSync; Object.defineProperty(exports, "__esModule", { value: true }); const fsStat = __webpack_require__(295); const fsWalk = __webpack_require__(300); -const reader_1 = __webpack_require__(794); +const reader_1 = __webpack_require__(795); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -93876,7 +93959,7 @@ exports.default = ReaderSync; /***/ }), -/* 805 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93940,7 +94023,7 @@ exports.default = Settings; /***/ }), -/* 806 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93948,7 +94031,7 @@ exports.default = Settings; const {promisify} = __webpack_require__(113); const fs = __webpack_require__(132); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(775); +const fastGlob = __webpack_require__(776); const gitIgnore = __webpack_require__(335); const slash = __webpack_require__(336); @@ -94067,7 +94150,7 @@ module.exports.sync = options => { /***/ }), -/* 807 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -94120,7 +94203,7 @@ module.exports = { /***/ }), -/* 808 */ +/* 809 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -94128,13 +94211,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return buildNonBazelProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getProductionProjects", function() { return getProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProject", function() { return buildProject; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(563); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(240); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(559); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(560); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(220); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(349); diff --git a/packages/kbn-pm/src/commands/index.ts b/packages/kbn-pm/src/commands/index.ts index 4c7992859ebdd..70e641f1e9351 100644 --- a/packages/kbn-pm/src/commands/index.ts +++ b/packages/kbn-pm/src/commands/index.ts @@ -32,6 +32,7 @@ import { CleanCommand } from './clean'; import { ResetCommand } from './reset'; import { RunCommand } from './run'; import { WatchCommand } from './watch'; +import { PatchNativeModulesCommand } from './patch_native_modules'; import { Kibana } from '../utils/kibana'; export const commands: { [key: string]: ICommand } = { @@ -41,4 +42,5 @@ export const commands: { [key: string]: ICommand } = { reset: ResetCommand, run: RunCommand, watch: WatchCommand, + patch_native_modules: PatchNativeModulesCommand, }; diff --git a/packages/kbn-pm/src/commands/patch_native_modules.ts b/packages/kbn-pm/src/commands/patch_native_modules.ts new file mode 100644 index 0000000000000..30fd599b83be3 --- /dev/null +++ b/packages/kbn-pm/src/commands/patch_native_modules.ts @@ -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. + */ + +import Path from 'path'; +import Fs from 'fs'; + +import { CiStatsReporter } from '@kbn/dev-utils/ci_stats_reporter'; + +import { log } from '../utils/log'; +import { spawn } from '../utils/child_process'; +import { ICommand } from './index'; + +export const PatchNativeModulesCommand: ICommand = { + description: 'Patch native modules by running build commands on M1 Macs', + name: 'patch_native_modules', + + async run(projects, _, { kbn }) { + const kibanaProjectPath = projects.get('kibana')?.path || ''; + const reporter = CiStatsReporter.fromEnv(log); + + if (process.platform !== 'darwin' || process.arch !== 'arm64') { + return; + } + + const startTime = Date.now(); + const nodeSassDir = Path.resolve(kibanaProjectPath, 'node_modules/node-sass'); + const nodeSassNativeDist = Path.resolve( + nodeSassDir, + `vendor/darwin-arm64-${process.versions.modules}/binding.node` + ); + if (!Fs.existsSync(nodeSassNativeDist)) { + log.info('Running build script for node-sass'); + await spawn('npm', ['run', 'build'], { + cwd: nodeSassDir, + }); + } + + const re2Dir = Path.resolve(kibanaProjectPath, 'node_modules/re2'); + const re2NativeDist = Path.resolve(re2Dir, 'build/Release/re2.node'); + if (!Fs.existsSync(re2NativeDist)) { + log.info('Running build script for re2'); + await spawn('npm', ['run', 'rebuild'], { + cwd: re2Dir, + }); + } + + log.success('native modules should be setup for native ARM Mac development'); + + // send timings + await reporter.timings({ + upstreamBranch: kbn.kibanaProject.json.branch, + // prevent loading @kbn/utils by passing null + kibanaUuid: kbn.getUuid() || null, + timings: [ + { + group: 'scripts/kbn bootstrap', + id: 'patch native modudles for arm macs', + ms: Date.now() - startTime, + }, + ], + }); + }, +}; From 7e4ae48efae6776c69f25bf8890359571c99e2c5 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 8 Nov 2021 19:15:46 -0500 Subject: [PATCH 16/98] [Alerting] Renaming alert instance summary to alert summary (#117023) * Renaming alert instance summary to alert summary * api docs * fixing types * updating functional test * Updating i18n and data test sub * fixing functional tests * Cleanup * Fixing unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- api_docs/alerting.json | 358 +++++++++--------- ...t_instance_summary.ts => alert_summary.ts} | 16 +- x-pack/plugins/alerting/common/index.ts | 2 +- ...s => alert_summary_from_event_log.test.ts} | 338 ++++++++--------- ...log.ts => alert_summary_from_event_log.ts} | 89 ++--- .../routes/get_rule_alert_summary.test.ts | 16 +- .../server/routes/get_rule_alert_summary.ts | 16 +- .../legacy/get_alert_instance_summary.test.ts | 18 +- .../legacy/get_alert_instance_summary.ts | 13 +- .../alerting/server/rules_client.mock.ts | 2 +- .../server/rules_client/rules_client.ts | 33 +- ...mary.test.ts => get_alert_summary.test.ts} | 132 +++---- .../translations/translations/ja-JP.json | 14 +- .../translations/translations/zh-CN.json | 14 +- .../lib/alert_api/alert_summary.test.ts | 16 +- .../lib/alert_api/alert_summary.ts | 22 +- .../public/application/lib/alert_api/index.ts | 2 +- .../components/alert_details.tsx | 8 +- .../{alert_instances.scss => alerts.scss} | 2 +- ...ert_instances.test.tsx => alerts.test.tsx} | 243 ++++++------ .../{alert_instances.tsx => alerts.tsx} | 187 +++++---- ...s_route.test.tsx => alerts_route.test.tsx} | 84 ++-- ...t_instances_route.tsx => alerts_route.tsx} | 52 ++- .../components/rule_muted_switch.tsx | 14 +- .../with_bulk_alert_api_operations.tsx | 10 +- .../triggers_actions_ui/public/types.ts | 8 +- ...stance_summary.ts => get_alert_summary.ts} | 36 +- .../tests/alerting/index.ts | 2 +- ...stance_summary.ts => get_alert_summary.ts} | 144 +++---- .../spaces_only/tests/alerting/index.ts | 2 +- .../apps/triggers_actions_ui/details.ts | 347 +++++++++-------- .../page_objects/index.ts | 4 +- .../{alert_details.ts => rule_details.ts} | 58 ++- 33 files changed, 1117 insertions(+), 1185 deletions(-) rename x-pack/plugins/alerting/common/{alert_instance_summary.ts => alert_summary.ts} (70%) rename x-pack/plugins/alerting/server/lib/{alert_instance_summary_from_event_log.test.ts => alert_summary_from_event_log.test.ts} (61%) rename x-pack/plugins/alerting/server/lib/{alert_instance_summary_from_event_log.ts => alert_summary_from_event_log.ts} (53%) rename x-pack/plugins/alerting/server/rules_client/tests/{get_alert_instance_summary.test.ts => get_alert_summary.test.ts} (71%) rename x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/{alert_instances.scss => alerts.scss} (87%) rename x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/{alert_instances.test.tsx => alerts.test.tsx} (61%) rename x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/{alert_instances.tsx => alerts.tsx} (59%) rename x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/{alert_instances_route.test.tsx => alerts_route.test.tsx} (59%) rename x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/{alert_instances_route.tsx => alerts_route.tsx} (52%) rename x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/{get_alert_instance_summary.ts => get_alert_summary.ts} (84%) rename x-pack/test/alerting_api_integration/spaces_only/tests/alerting/{get_alert_instance_summary.ts => get_alert_summary.ts} (67%) rename x-pack/test/functional_with_es_ssl/page_objects/{alert_details.ts => rule_details.ts} (60%) diff --git a/api_docs/alerting.json b/api_docs/alerting.json index 2b82778439707..b0d37c25a10aa 100644 --- a/api_docs/alerting.json +++ b/api_docs/alerting.json @@ -1793,7 +1793,7 @@ "section": "def-server.RulesClient", "text": "RulesClient" }, - ", \"create\" | \"delete\" | \"find\" | \"get\" | \"resolve\" | \"update\" | \"aggregate\" | \"enable\" | \"disable\" | \"muteAll\" | \"getAlertState\" | \"getAlertInstanceSummary\" | \"updateApiKey\" | \"unmuteAll\" | \"muteInstance\" | \"unmuteInstance\" | \"listAlertTypes\" | \"getSpaceId\">" + ", \"create\" | \"delete\" | \"find\" | \"get\" | \"resolve\" | \"update\" | \"aggregate\" | \"enable\" | \"disable\" | \"muteAll\" | \"getAlertState\" | \"getAlertSummary\" | \"updateApiKey\" | \"unmuteAll\" | \"muteInstance\" | \"unmuteInstance\" | \"listAlertTypes\" | \"getSpaceId\">" ], "path": "x-pack/plugins/alerting/server/plugin.ts", "deprecated": false, @@ -2223,15 +2223,15 @@ "AggregateOptions", " | undefined; }) => Promise<", "AggregateResult", - ">; enable: ({ id }: { id: string; }) => Promise; disable: ({ id }: { id: string; }) => Promise; muteAll: ({ id }: { id: string; }) => Promise; getAlertState: ({ id }: { id: string; }) => Promise; getAlertInstanceSummary: ({ id, dateStart, }: ", - "GetAlertInstanceSummaryParams", + ">; enable: ({ id }: { id: string; }) => Promise; disable: ({ id }: { id: string; }) => Promise; muteAll: ({ id }: { id: string; }) => Promise; getAlertState: ({ id }: { id: string; }) => Promise; getAlertSummary: ({ id, dateStart }: ", + "GetAlertSummaryParams", ") => Promise<", { "pluginId": "alerting", "scope": "common", "docId": "kibAlertingPluginApi", - "section": "def-common.AlertInstanceSummary", - "text": "AlertInstanceSummary" + "section": "def-common.AlertSummary", + "text": "AlertSummary" }, ">; updateApiKey: ({ id }: { id: string; }) => Promise; unmuteAll: ({ id }: { id: string; }) => Promise; muteInstance: ({ alertId, alertInstanceId }: ", "MuteOptions", @@ -3157,17 +3157,125 @@ }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus", + "id": "def-common.AlertsHealth", "type": "Interface", "tags": [], - "label": "AlertInstanceStatus", + "label": "AlertsHealth", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert.ts", "deprecated": false, "children": [ { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.status", + "id": "def-common.AlertsHealth.decryptionHealth", + "type": "Object", + "tags": [], + "label": "decryptionHealth", + "description": [], + "signature": [ + "{ status: ", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.HealthStatus", + "text": "HealthStatus" + }, + "; timestamp: string; }" + ], + "path": "x-pack/plugins/alerting/common/alert.ts", + "deprecated": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertsHealth.executionHealth", + "type": "Object", + "tags": [], + "label": "executionHealth", + "description": [], + "signature": [ + "{ status: ", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.HealthStatus", + "text": "HealthStatus" + }, + "; timestamp: string; }" + ], + "path": "x-pack/plugins/alerting/common/alert.ts", + "deprecated": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertsHealth.readHealth", + "type": "Object", + "tags": [], + "label": "readHealth", + "description": [], + "signature": [ + "{ status: ", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.HealthStatus", + "text": "HealthStatus" + }, + "; timestamp: string; }" + ], + "path": "x-pack/plugins/alerting/common/alert.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertStateNavigation", + "type": "Interface", + "tags": [], + "label": "AlertStateNavigation", + "description": [], + "path": "x-pack/plugins/alerting/common/alert_navigation.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "alerting", + "id": "def-common.AlertStateNavigation.state", + "type": "Object", + "tags": [], + "label": "state", + "description": [], + "signature": [ + { + "pluginId": "@kbn/utility-types", + "scope": "server", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-server.JsonObject", + "text": "JsonObject" + } + ], + "path": "x-pack/plugins/alerting/common/alert_navigation.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertStatus", + "type": "Interface", + "tags": [], + "label": "AlertStatus", + "description": [], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "alerting", + "id": "def-common.AlertStatus.status", "type": "CompoundType", "tags": [], "label": "status", @@ -3175,22 +3283,22 @@ "signature": [ "\"OK\" | \"Active\"" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.muted", + "id": "def-common.AlertStatus.muted", "type": "boolean", "tags": [], "label": "muted", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.actionGroupId", + "id": "def-common.AlertStatus.actionGroupId", "type": "string", "tags": [], "label": "actionGroupId", @@ -3198,12 +3306,12 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.actionSubgroup", + "id": "def-common.AlertStatus.actionSubgroup", "type": "string", "tags": [], "label": "actionSubgroup", @@ -3211,12 +3319,12 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatus.activeStartDate", + "id": "def-common.AlertStatus.activeStartDate", "type": "string", "tags": [], "label": "activeStartDate", @@ -3224,7 +3332,7 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false } ], @@ -3232,37 +3340,37 @@ }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary", + "id": "def-common.AlertSummary", "type": "Interface", "tags": [], - "label": "AlertInstanceSummary", + "label": "AlertSummary", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "children": [ { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.id", + "id": "def-common.AlertSummary.id", "type": "string", "tags": [], "label": "id", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.name", + "id": "def-common.AlertSummary.name", "type": "string", "tags": [], "label": "name", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.tags", + "id": "def-common.AlertSummary.tags", "type": "Array", "tags": [], "label": "tags", @@ -3270,42 +3378,42 @@ "signature": [ "string[]" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.alertTypeId", + "id": "def-common.AlertSummary.ruleTypeId", "type": "string", "tags": [], - "label": "alertTypeId", + "label": "ruleTypeId", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.consumer", + "id": "def-common.AlertSummary.consumer", "type": "string", "tags": [], "label": "consumer", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.muteAll", + "id": "def-common.AlertSummary.muteAll", "type": "boolean", "tags": [], "label": "muteAll", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.throttle", + "id": "def-common.AlertSummary.throttle", "type": "CompoundType", "tags": [], "label": "throttle", @@ -3313,42 +3421,42 @@ "signature": [ "string | null" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.enabled", + "id": "def-common.AlertSummary.enabled", "type": "boolean", "tags": [], "label": "enabled", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.statusStartDate", + "id": "def-common.AlertSummary.statusStartDate", "type": "string", "tags": [], "label": "statusStartDate", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.statusEndDate", + "id": "def-common.AlertSummary.statusEndDate", "type": "string", "tags": [], "label": "statusEndDate", "description": [], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.status", + "id": "def-common.AlertSummary.status", "type": "CompoundType", "tags": [], "label": "status", @@ -3356,12 +3464,12 @@ "signature": [ "\"OK\" | \"Active\" | \"Error\"" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.lastRun", + "id": "def-common.AlertSummary.lastRun", "type": "string", "tags": [], "label": "lastRun", @@ -3369,12 +3477,12 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.errorMessages", + "id": "def-common.AlertSummary.errorMessages", "type": "Array", "tags": [], "label": "errorMessages", @@ -3382,15 +3490,15 @@ "signature": [ "{ date: string; message: string; }[]" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.instances", + "id": "def-common.AlertSummary.alerts", "type": "Object", "tags": [], - "label": "instances", + "label": "alerts", "description": [], "signature": [ "{ [x: string]: ", @@ -3398,17 +3506,17 @@ "pluginId": "alerting", "scope": "common", "docId": "kibAlertingPluginApi", - "section": "def-common.AlertInstanceStatus", - "text": "AlertInstanceStatus" + "section": "def-common.AlertStatus", + "text": "AlertStatus" }, "; }" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-common.AlertInstanceSummary.executionDuration", + "id": "def-common.AlertSummary.executionDuration", "type": "Object", "tags": [], "label": "executionDuration", @@ -3416,115 +3524,7 @@ "signature": [ "{ average: number; values: number[]; }" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertsHealth", - "type": "Interface", - "tags": [], - "label": "AlertsHealth", - "description": [], - "path": "x-pack/plugins/alerting/common/alert.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "alerting", - "id": "def-common.AlertsHealth.decryptionHealth", - "type": "Object", - "tags": [], - "label": "decryptionHealth", - "description": [], - "signature": [ - "{ status: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.HealthStatus", - "text": "HealthStatus" - }, - "; timestamp: string; }" - ], - "path": "x-pack/plugins/alerting/common/alert.ts", - "deprecated": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertsHealth.executionHealth", - "type": "Object", - "tags": [], - "label": "executionHealth", - "description": [], - "signature": [ - "{ status: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.HealthStatus", - "text": "HealthStatus" - }, - "; timestamp: string; }" - ], - "path": "x-pack/plugins/alerting/common/alert.ts", - "deprecated": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertsHealth.readHealth", - "type": "Object", - "tags": [], - "label": "readHealth", - "description": [], - "signature": [ - "{ status: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.HealthStatus", - "text": "HealthStatus" - }, - "; timestamp: string; }" - ], - "path": "x-pack/plugins/alerting/common/alert.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertStateNavigation", - "type": "Interface", - "tags": [], - "label": "AlertStateNavigation", - "description": [], - "path": "x-pack/plugins/alerting/common/alert_navigation.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "alerting", - "id": "def-common.AlertStateNavigation.state", - "type": "Object", - "tags": [], - "label": "state", - "description": [], - "signature": [ - { - "pluginId": "@kbn/utility-types", - "scope": "server", - "docId": "kibKbnUtilityTypesPluginApi", - "section": "def-server.JsonObject", - "text": "JsonObject" - } - ], - "path": "x-pack/plugins/alerting/common/alert_navigation.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false } ], @@ -3912,20 +3912,6 @@ "deprecated": false, "initialIsOpen": false }, - { - "parentPluginId": "alerting", - "id": "def-common.AlertInstanceStatusValues", - "type": "Type", - "tags": [], - "label": "AlertInstanceStatusValues", - "description": [], - "signature": [ - "\"OK\" | \"Active\"" - ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", - "deprecated": false, - "initialIsOpen": false - }, { "parentPluginId": "alerting", "id": "def-common.AlertNavigation", @@ -3990,9 +3976,9 @@ "label": "AlertStatusValues", "description": [], "signature": [ - "\"OK\" | \"Active\" | \"Error\"" + "\"OK\" | \"Active\"" ], - "path": "x-pack/plugins/alerting/common/alert_instance_summary.ts", + "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "initialIsOpen": false }, @@ -4180,6 +4166,20 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "alerting", + "id": "def-common.RuleStatusValues", + "type": "Type", + "tags": [], + "label": "RuleStatusValues", + "description": [], + "signature": [ + "\"OK\" | \"Active\" | \"Error\"" + ], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "alerting", "id": "def-common.SanitizedAlert", diff --git a/x-pack/plugins/alerting/common/alert_instance_summary.ts b/x-pack/plugins/alerting/common/alert_summary.ts similarity index 70% rename from x-pack/plugins/alerting/common/alert_instance_summary.ts rename to x-pack/plugins/alerting/common/alert_summary.ts index 462d20b8fb2ea..87d05dce7c958 100644 --- a/x-pack/plugins/alerting/common/alert_instance_summary.ts +++ b/x-pack/plugins/alerting/common/alert_summary.ts @@ -5,32 +5,32 @@ * 2.0. */ -export type AlertStatusValues = 'OK' | 'Active' | 'Error'; -export type AlertInstanceStatusValues = 'OK' | 'Active'; +export type RuleStatusValues = 'OK' | 'Active' | 'Error'; +export type AlertStatusValues = 'OK' | 'Active'; -export interface AlertInstanceSummary { +export interface AlertSummary { id: string; name: string; tags: string[]; - alertTypeId: string; + ruleTypeId: string; consumer: string; muteAll: boolean; throttle: string | null; enabled: boolean; statusStartDate: string; statusEndDate: string; - status: AlertStatusValues; + status: RuleStatusValues; lastRun?: string; errorMessages: Array<{ date: string; message: string }>; - instances: Record; + alerts: Record; executionDuration: { average: number; values: number[]; }; } -export interface AlertInstanceStatus { - status: AlertInstanceStatusValues; +export interface AlertStatus { + status: AlertStatusValues; muted: boolean; actionGroupId?: string; actionSubgroup?: string; diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 61e38941d0233..1c7525a065760 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -15,7 +15,7 @@ export * from './alert_type'; export * from './alert_instance'; export * from './alert_task_instance'; export * from './alert_navigation'; -export * from './alert_instance_summary'; +export * from './alert_summary'; export * from './builtin_action_groups'; export * from './disabled_action_groups'; export * from './alert_notify_when_type'; diff --git a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts similarity index 61% rename from x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts rename to x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts index a6529f4c30a7b..e243a4dc0ad5b 100644 --- a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts @@ -6,21 +6,21 @@ */ import { random, mean } from 'lodash'; -import { SanitizedAlert, AlertInstanceSummary } from '../types'; +import { SanitizedAlert, AlertSummary } from '../types'; import { IValidatedEvent } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; -import { alertInstanceSummaryFromEventLog } from './alert_instance_summary_from_event_log'; +import { alertSummaryFromEventLog } from './alert_summary_from_event_log'; const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000; const dateStart = '2020-06-18T00:00:00.000Z'; const dateEnd = dateString(dateStart, ONE_HOUR_IN_MILLIS); -describe('alertInstanceSummaryFromEventLog', () => { +describe('alertSummaryFromEventLog', () => { test('no events and muted ids', async () => { - const alert = createAlert({}); + const rule = createRule({}); const events: IValidatedEvent[] = []; - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, @@ -28,19 +28,19 @@ describe('alertInstanceSummaryFromEventLog', () => { expect(summary).toMatchInlineSnapshot(` Object { - "alertTypeId": "123", - "consumer": "alert-consumer", + "alerts": Object {}, + "consumer": "rule-consumer", "enabled": false, "errorMessages": Array [], "executionDuration": Object { "average": 0, "values": Array [], }, - "id": "alert-123", - "instances": Object {}, + "id": "rule-123", "lastRun": undefined, "muteAll": false, - "name": "alert-name", + "name": "rule-name", + "ruleTypeId": "123", "status": "OK", "statusEndDate": "2020-06-18T01:00:00.000Z", "statusStartDate": "2020-06-18T00:00:00.000Z", @@ -50,21 +50,21 @@ describe('alertInstanceSummaryFromEventLog', () => { `); }); - test('different alert properties', async () => { - const alert = createAlert({ - id: 'alert-456', + test('different rule properties', async () => { + const rule = createRule({ + id: 'rule-456', alertTypeId: '456', schedule: { interval: '100s' }, enabled: true, - name: 'alert-name-2', + name: 'rule-name-2', tags: ['tag-1', 'tag-2'], - consumer: 'alert-consumer-2', + consumer: 'rule-consumer-2', throttle: '1h', muteAll: true, }); const events: IValidatedEvent[] = []; - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart: dateString(dateEnd, ONE_HOUR_IN_MILLIS), dateEnd: dateString(dateEnd, ONE_HOUR_IN_MILLIS * 2), @@ -72,19 +72,19 @@ describe('alertInstanceSummaryFromEventLog', () => { expect(summary).toMatchInlineSnapshot(` Object { - "alertTypeId": "456", - "consumer": "alert-consumer-2", + "alerts": Object {}, + "consumer": "rule-consumer-2", "enabled": true, "errorMessages": Array [], "executionDuration": Object { "average": 0, "values": Array [], }, - "id": "alert-456", - "instances": Object {}, + "id": "rule-456", "lastRun": undefined, "muteAll": true, - "name": "alert-name-2", + "name": "rule-name-2", + "ruleTypeId": "456", "status": "OK", "statusEndDate": "2020-06-18T03:00:00.000Z", "statusStartDate": "2020-06-18T02:00:00.000Z", @@ -97,30 +97,30 @@ describe('alertInstanceSummaryFromEventLog', () => { `); }); - test('two muted instances', async () => { - const alert = createAlert({ - mutedInstanceIds: ['instance-1', 'instance-2'], + test('two muted alerts', async () => { + const rule = createRule({ + mutedInstanceIds: ['alert-1', 'alert-2'], }); const events: IValidatedEvent[] = []; - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", }, - "instance-2": Object { + "alert-2": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, @@ -134,22 +134,22 @@ describe('alertInstanceSummaryFromEventLog', () => { `); }); - test('active alert but no instances', async () => { - const alert = createAlert({}); + test('active rule but no alerts', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory.addExecute().advanceTime(10000).addExecute().getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object {}, + "alerts": Object {}, "lastRun": "2020-06-18T00:00:10.000Z", "status": "OK", } @@ -158,8 +158,8 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('active alert with no instances but has errors', async () => { - const alert = createAlert({}); + test('active rule with no alerts but has errors', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute('oof!') @@ -167,16 +167,17 @@ describe('alertInstanceSummaryFromEventLog', () => { .addExecute('rut roh!') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, errorMessages, instances, executionDuration } = summary; - expect({ lastRun, status, errorMessages, instances }).toMatchInlineSnapshot(` + const { lastRun, status, errorMessages, alerts, executionDuration } = summary; + expect({ lastRun, status, errorMessages, alerts }).toMatchInlineSnapshot(` Object { + "alerts": Object {}, "errorMessages": Array [ Object { "date": "2020-06-18T00:00:00.000Z", @@ -187,7 +188,6 @@ describe('alertInstanceSummaryFromEventLog', () => { "message": "rut roh!", }, ], - "instances": Object {}, "lastRun": "2020-06-18T00:00:10.000Z", "status": "Error", } @@ -196,30 +196,30 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('alert with currently inactive instance', async () => { - const alert = createAlert({}); + test('rule with currently inactive alert', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') .advanceTime(10000) .addExecute() - .addRecoveredInstance('instance-1') + .addRecoveredAlert('alert-1') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, @@ -235,30 +235,30 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('legacy alert with currently inactive instance', async () => { - const alert = createAlert({}); + test('legacy rule with currently inactive alert', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') .advanceTime(10000) .addExecute() - .addLegacyResolvedInstance('instance-1') + .addLegacyResolvedAlert('alert-1') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, @@ -274,29 +274,29 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('alert with currently inactive instance, no new-instance', async () => { - const alert = createAlert({}); + test('rule with currently inactive alert, no new-instance', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addActiveInstance('instance-1', 'action group A') + .addActiveAlert('alert-1', 'action group A') .advanceTime(10000) .addExecute() - .addRecoveredInstance('instance-1') + .addRecoveredAlert('alert-1') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, @@ -312,30 +312,30 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('alert with currently active instance', async () => { - const alert = createAlert({}); + test('rule with currently active alert', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1', 'action group A') + .addActiveAlert('alert-1', 'action group A') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": "action group A", "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", @@ -351,30 +351,30 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('alert with currently active instance with no action group in event log', async () => { - const alert = createAlert({}); + test('rule with currently active alert with no action group in event log', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', undefined) + .addNewAlert('alert-1') + .addActiveAlert('alert-1', undefined) .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1', undefined) + .addActiveAlert('alert-1', undefined) .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", @@ -390,30 +390,30 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('alert with currently active instance that switched action groups', async () => { - const alert = createAlert({}); + test('rule with currently active alert that switched action groups', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1', 'action group B') + .addActiveAlert('alert-1', 'action group B') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": "action group B", "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", @@ -429,29 +429,29 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('alert with currently active instance, no new-instance', async () => { - const alert = createAlert({}); + test('rule with currently active alert, no new-instance', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addActiveInstance('instance-1', 'action group A') + .addActiveAlert('alert-1', 'action group A') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1', 'action group A') + .addActiveAlert('alert-1', 'action group A') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": "action group A", "actionSubgroup": undefined, "activeStartDate": undefined, @@ -467,40 +467,40 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('alert with active and inactive muted alerts', async () => { - const alert = createAlert({ mutedInstanceIds: ['instance-1', 'instance-2'] }); + test('rule with active and inactive muted alerts', async () => { + const rule = createRule({ mutedInstanceIds: ['alert-1', 'alert-2'] }); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .addNewInstance('instance-2') - .addActiveInstance('instance-2', 'action group B') + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') + .addNewAlert('alert-2') + .addActiveAlert('alert-2', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1', 'action group A') - .addRecoveredInstance('instance-2') + .addActiveAlert('alert-1', 'action group A') + .addRecoveredAlert('alert-2') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": "action group A", "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": true, "status": "Active", }, - "instance-2": Object { + "alert-2": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, @@ -516,46 +516,46 @@ describe('alertInstanceSummaryFromEventLog', () => { testExecutionDurations(eventsFactory.getExecutionDurations(), executionDuration); }); - test('alert with active and inactive alerts over many executes', async () => { - const alert = createAlert({}); + test('rule with active and inactive alerts over many executes', async () => { + const rule = createRule({}); const eventsFactory = new EventsFactory(); const events = eventsFactory .addExecute() - .addNewInstance('instance-1') - .addActiveInstance('instance-1', 'action group A') - .addNewInstance('instance-2') - .addActiveInstance('instance-2', 'action group B') + .addNewAlert('alert-1') + .addActiveAlert('alert-1', 'action group A') + .addNewAlert('alert-2') + .addActiveAlert('alert-2', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1', 'action group A') - .addRecoveredInstance('instance-2') + .addActiveAlert('alert-1', 'action group A') + .addRecoveredAlert('alert-2') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1', 'action group B') + .addActiveAlert('alert-1', 'action group B') .advanceTime(10000) .addExecute() - .addActiveInstance('instance-1', 'action group B') + .addActiveAlert('alert-1', 'action group B') .getEvents(); - const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({ - alert, + const summary: AlertSummary = alertSummaryFromEventLog({ + rule, events, dateStart, dateEnd, }); - const { lastRun, status, instances, executionDuration } = summary; - expect({ lastRun, status, instances }).toMatchInlineSnapshot(` + const { lastRun, status, alerts, executionDuration } = summary; + expect({ lastRun, status, alerts }).toMatchInlineSnapshot(` Object { - "instances": Object { - "instance-1": Object { + "alerts": Object { + "alert-1": Object { "actionGroupId": "action group B", "actionSubgroup": undefined, "activeStartDate": "2020-06-18T00:00:00.000Z", "muted": false, "status": "Active", }, - "instance-2": Object { + "alert-2": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, @@ -626,10 +626,10 @@ export class EventsFactory { return this; } - addActiveInstance(instanceId: string, actionGroupId: string | undefined): EventsFactory { + addActiveAlert(alertId: string, actionGroupId: string | undefined): EventsFactory { const kibanaAlerting = actionGroupId - ? { instance_id: instanceId, action_group_id: actionGroupId } - : { instance_id: instanceId }; + ? { instance_id: alertId, action_group_id: actionGroupId } + : { instance_id: alertId }; this.events.push({ '@timestamp': this.date, event: { @@ -641,38 +641,38 @@ export class EventsFactory { return this; } - addNewInstance(instanceId: string): EventsFactory { + addNewAlert(alertId: string): EventsFactory { this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, action: EVENT_LOG_ACTIONS.newInstance, }, - kibana: { alerting: { instance_id: instanceId } }, + kibana: { alerting: { instance_id: alertId } }, }); return this; } - addRecoveredInstance(instanceId: string): EventsFactory { + addRecoveredAlert(alertId: string): EventsFactory { this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, action: EVENT_LOG_ACTIONS.recoveredInstance, }, - kibana: { alerting: { instance_id: instanceId } }, + kibana: { alerting: { instance_id: alertId } }, }); return this; } - addLegacyResolvedInstance(instanceId: string): EventsFactory { + addLegacyResolvedAlert(alertId: string): EventsFactory { this.events.push({ '@timestamp': this.date, event: { provider: EVENT_LOG_PROVIDER, action: LEGACY_EVENT_LOG_ACTIONS.resolvedInstance, }, - kibana: { alerting: { instance_id: instanceId } }, + kibana: { alerting: { instance_id: alertId } }, }); return this; } @@ -684,18 +684,18 @@ export class EventsFactory { } } -function createAlert(overrides: Partial): SanitizedAlert<{ bar: boolean }> { - return { ...BaseAlert, ...overrides }; +function createRule(overrides: Partial): SanitizedAlert<{ bar: boolean }> { + return { ...BaseRule, ...overrides }; } -const BaseAlert: SanitizedAlert<{ bar: boolean }> = { - id: 'alert-123', +const BaseRule: SanitizedAlert<{ bar: boolean }> = { + id: 'rule-123', alertTypeId: '123', schedule: { interval: '10s' }, enabled: false, - name: 'alert-name', + name: 'rule-name', tags: [], - consumer: 'alert-consumer', + consumer: 'rule-consumer', throttle: null, notifyWhen: null, muteAll: false, diff --git a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts similarity index 53% rename from x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts rename to x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts index 40fae121df51d..65c29407a8a01 100644 --- a/x-pack/plugins/alerting/server/lib/alert_instance_summary_from_event_log.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts @@ -6,46 +6,44 @@ */ import { mean } from 'lodash'; -import { SanitizedAlert, AlertInstanceSummary, AlertInstanceStatus } from '../types'; +import { SanitizedAlert, AlertSummary, AlertStatus } from '../types'; import { IEvent } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin'; const Millis2Nanos = 1000 * 1000; -export interface AlertInstanceSummaryFromEventLogParams { - alert: SanitizedAlert<{ bar: boolean }>; +export interface AlertSummaryFromEventLogParams { + rule: SanitizedAlert<{ bar: boolean }>; events: IEvent[]; dateStart: string; dateEnd: string; } -export function alertInstanceSummaryFromEventLog( - params: AlertInstanceSummaryFromEventLogParams -): AlertInstanceSummary { +export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams): AlertSummary { // initialize the result - const { alert, events, dateStart, dateEnd } = params; - const alertInstanceSummary: AlertInstanceSummary = { - id: alert.id, - name: alert.name, - tags: alert.tags, - alertTypeId: alert.alertTypeId, - consumer: alert.consumer, + const { rule, events, dateStart, dateEnd } = params; + const alertSummary: AlertSummary = { + id: rule.id, + name: rule.name, + tags: rule.tags, + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, statusStartDate: dateStart, statusEndDate: dateEnd, status: 'OK', - muteAll: alert.muteAll, - throttle: alert.throttle, - enabled: alert.enabled, + muteAll: rule.muteAll, + throttle: rule.throttle, + enabled: rule.enabled, lastRun: undefined, errorMessages: [], - instances: {}, + alerts: {}, executionDuration: { average: 0, values: [], }, }; - const instances = new Map(); + const alerts = new Map(); const eventDurations: number[] = []; // loop through the events @@ -61,17 +59,17 @@ export function alertInstanceSummaryFromEventLog( if (action === undefined) continue; if (action === EVENT_LOG_ACTIONS.execute) { - alertInstanceSummary.lastRun = timeStamp; + alertSummary.lastRun = timeStamp; const errorMessage = event?.error?.message; if (errorMessage !== undefined) { - alertInstanceSummary.status = 'Error'; - alertInstanceSummary.errorMessages.push({ + alertSummary.status = 'Error'; + alertSummary.errorMessages.push({ date: timeStamp, message: errorMessage, }); } else { - alertInstanceSummary.status = 'OK'; + alertSummary.status = 'OK'; } if (event?.event?.duration) { @@ -81,10 +79,10 @@ export function alertInstanceSummaryFromEventLog( continue; } - const instanceId = event?.kibana?.alerting?.instance_id; - if (instanceId === undefined) continue; + const alertId = event?.kibana?.alerting?.instance_id; + if (alertId === undefined) continue; - const status = getAlertInstanceStatus(instances, instanceId); + const status = getAlertStatus(alerts, alertId); switch (action) { case EVENT_LOG_ACTIONS.newInstance: status.activeStartDate = timeStamp; @@ -103,50 +101,47 @@ export function alertInstanceSummaryFromEventLog( } } - // set the muted status of instances - for (const instanceId of alert.mutedInstanceIds) { - getAlertInstanceStatus(instances, instanceId).muted = true; + // set the muted status of alerts + for (const alertId of rule.mutedInstanceIds) { + getAlertStatus(alerts, alertId).muted = true; } - // convert the instances map to object form - const instanceIds = Array.from(instances.keys()).sort(); - for (const instanceId of instanceIds) { - alertInstanceSummary.instances[instanceId] = instances.get(instanceId)!; + // convert the alerts map to object form + const alertIds = Array.from(alerts.keys()).sort(); + for (const alertId of alertIds) { + alertSummary.alerts[alertId] = alerts.get(alertId)!; } - // set the overall alert status to Active if appropriate - if (alertInstanceSummary.status !== 'Error') { - if (Array.from(instances.values()).some((instance) => instance.status === 'Active')) { - alertInstanceSummary.status = 'Active'; + // set the overall alert status to Active if appropriatea + if (alertSummary.status !== 'Error') { + if (Array.from(alerts.values()).some((a) => a.status === 'Active')) { + alertSummary.status = 'Active'; } } - alertInstanceSummary.errorMessages.sort((a, b) => a.date.localeCompare(b.date)); + alertSummary.errorMessages.sort((a, b) => a.date.localeCompare(b.date)); if (eventDurations.length > 0) { - alertInstanceSummary.executionDuration = { + alertSummary.executionDuration = { average: Math.round(mean(eventDurations)), values: eventDurations, }; } - return alertInstanceSummary; + return alertSummary; } -// return an instance status object, creating and adding to the map if needed -function getAlertInstanceStatus( - instances: Map, - instanceId: string -): AlertInstanceStatus { - if (instances.has(instanceId)) return instances.get(instanceId)!; +// return an alert status object, creating and adding to the map if needed +function getAlertStatus(alerts: Map, alertId: string): AlertStatus { + if (alerts.has(alertId)) return alerts.get(alertId)!; - const status: AlertInstanceStatus = { + const status: AlertStatus = { status: 'OK', muted: false, actionGroupId: undefined, actionSubgroup: undefined, activeStartDate: undefined, }; - instances.set(instanceId, status); + alerts.set(alertId, status); return status; } diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts index 5044c5c8617a0..7b7088a127491 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts @@ -11,7 +11,7 @@ import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { SavedObjectsErrorHelpers } from 'src/core/server'; import { rulesClientMock } from '../rules_client.mock'; -import { AlertInstanceSummary } from '../types'; +import { AlertSummary } from '../types'; const rulesClient = rulesClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -24,11 +24,11 @@ beforeEach(() => { describe('getRuleAlertSummaryRoute', () => { const dateString = new Date().toISOString(); - const mockedAlertInstanceSummary: AlertInstanceSummary = { + const mockedAlertSummary: AlertSummary = { id: '', name: '', tags: [], - alertTypeId: '', + ruleTypeId: '', consumer: '', muteAll: false, throttle: null, @@ -37,7 +37,7 @@ describe('getRuleAlertSummaryRoute', () => { statusEndDate: dateString, status: 'OK', errorMessages: [], - instances: {}, + alerts: {}, executionDuration: { average: 1, values: [3, 5, 5], @@ -54,7 +54,7 @@ describe('getRuleAlertSummaryRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_alert_summary"`); - rulesClient.getAlertInstanceSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); + rulesClient.getAlertSummary.mockResolvedValueOnce(mockedAlertSummary); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -69,8 +69,8 @@ describe('getRuleAlertSummaryRoute', () => { await handler(context, req, res); - expect(rulesClient.getAlertInstanceSummary).toHaveBeenCalledTimes(1); - expect(rulesClient.getAlertInstanceSummary.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.getAlertSummary).toHaveBeenCalledTimes(1); + expect(rulesClient.getAlertSummary.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "dateStart": undefined, @@ -90,7 +90,7 @@ describe('getRuleAlertSummaryRoute', () => { const [, handler] = router.get.mock.calls[0]; - rulesClient.getAlertInstanceSummary = jest + rulesClient.getAlertSummary = jest .fn() .mockResolvedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts index 131bddcce049d..dbe71c09d7402 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.ts @@ -8,12 +8,12 @@ import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../lib'; -import { GetAlertInstanceSummaryParams } from '../rules_client'; +import { GetAlertSummaryParams } from '../rules_client'; import { RewriteRequestCase, RewriteResponseCase, verifyAccessAndContext } from './lib'; import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH, - AlertInstanceSummary, + AlertSummary, } from '../types'; const paramSchema = schema.object({ @@ -24,27 +24,25 @@ const querySchema = schema.object({ date_start: schema.maybe(schema.string()), }); -const rewriteReq: RewriteRequestCase = ({ +const rewriteReq: RewriteRequestCase = ({ date_start: dateStart, ...rest }) => ({ ...rest, dateStart, }); -const rewriteBodyRes: RewriteResponseCase = ({ - alertTypeId, +const rewriteBodyRes: RewriteResponseCase = ({ + ruleTypeId, muteAll, statusStartDate, statusEndDate, errorMessages, lastRun, - instances: alerts, executionDuration, ...rest }) => ({ ...rest, - alerts, - rule_type_id: alertTypeId, + rule_type_id: ruleTypeId, mute_all: muteAll, status_start_date: statusStartDate, status_end_date: statusEndDate, @@ -69,7 +67,7 @@ export const getRuleAlertSummaryRoute = ( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = context.alerting.getRulesClient(); const { id } = req.params; - const summary = await rulesClient.getAlertInstanceSummary(rewriteReq({ id, ...req.query })); + const summary = await rulesClient.getAlertSummary(rewriteReq({ id, ...req.query })); return res.ok({ body: rewriteBodyRes(summary) }); }) ) diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts index ed2b3056da45e..e5ce9c5f3e285 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts @@ -11,7 +11,7 @@ import { licenseStateMock } from '../../lib/license_state.mock'; import { mockHandlerArguments } from './../_mock_handler_arguments'; import { SavedObjectsErrorHelpers } from 'src/core/server'; import { rulesClientMock } from '../../rules_client.mock'; -import { AlertInstanceSummary } from '../../types'; +import { AlertSummary } from '../../types'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; const rulesClient = rulesClientMock.create(); @@ -29,11 +29,11 @@ beforeEach(() => { describe('getAlertInstanceSummaryRoute', () => { const dateString = new Date().toISOString(); - const mockedAlertInstanceSummary: AlertInstanceSummary = { + const mockedAlertInstanceSummary: AlertSummary = { id: '', name: '', tags: [], - alertTypeId: '', + ruleTypeId: '', consumer: '', muteAll: false, throttle: null, @@ -42,7 +42,7 @@ describe('getAlertInstanceSummaryRoute', () => { statusEndDate: dateString, status: 'OK', errorMessages: [], - instances: {}, + alerts: {}, executionDuration: { average: 0, values: [], @@ -59,7 +59,7 @@ describe('getAlertInstanceSummaryRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_instance_summary"`); - rulesClient.getAlertInstanceSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); + rulesClient.getAlertSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -74,8 +74,8 @@ describe('getAlertInstanceSummaryRoute', () => { await handler(context, req, res); - expect(rulesClient.getAlertInstanceSummary).toHaveBeenCalledTimes(1); - expect(rulesClient.getAlertInstanceSummary.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.getAlertSummary).toHaveBeenCalledTimes(1); + expect(rulesClient.getAlertSummary.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "dateStart": undefined, @@ -95,7 +95,7 @@ describe('getAlertInstanceSummaryRoute', () => { const [, handler] = router.get.mock.calls[0]; - rulesClient.getAlertInstanceSummary = jest + rulesClient.getAlertSummary = jest .fn() .mockResolvedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); @@ -121,6 +121,8 @@ describe('getAlertInstanceSummaryRoute', () => { getAlertInstanceSummaryRoute(router, licenseState, mockUsageCounter); const [, handler] = router.get.mock.calls[0]; + + rulesClient.getAlertSummary.mockResolvedValueOnce(mockedAlertInstanceSummary); const [context, req, res] = mockHandlerArguments( { rulesClient }, { params: { id: '1' }, query: {} }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts index 2eed14a913a85..e94c0a858646d 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts @@ -10,7 +10,7 @@ import { UsageCounter } from 'src/plugins/usage_collection/server'; import type { AlertingRouter } from '../../types'; import { ILicenseState } from '../../lib/license_state'; import { verifyApiAccess } from '../../lib/license_api_access'; -import { LEGACY_BASE_ALERT_API_PATH } from '../../../common'; +import { AlertSummary, LEGACY_BASE_ALERT_API_PATH } from '../../../common'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; const paramSchema = schema.object({ @@ -21,6 +21,12 @@ const querySchema = schema.object({ dateStart: schema.maybe(schema.string()), }); +const rewriteBodyRes = ({ ruleTypeId, alerts, ...rest }: AlertSummary) => ({ + ...rest, + alertTypeId: ruleTypeId, + instances: alerts, +}); + export const getAlertInstanceSummaryRoute = ( router: AlertingRouter, licenseState: ILicenseState, @@ -43,8 +49,9 @@ export const getAlertInstanceSummaryRoute = ( const rulesClient = context.alerting.getRulesClient(); const { id } = req.params; const { dateStart } = req.query; - const summary = await rulesClient.getAlertInstanceSummary({ id, dateStart }); - return res.ok({ body: summary }); + const summary = await rulesClient.getAlertSummary({ id, dateStart }); + + return res.ok({ body: rewriteBodyRes(summary) }); }) ); }; diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 438331a1cd580..2395e7f041846 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -29,7 +29,7 @@ const createRulesClientMock = () => { muteInstance: jest.fn(), unmuteInstance: jest.fn(), listAlertTypes: jest.fn(), - getAlertInstanceSummary: jest.fn(), + getAlertSummary: jest.fn(), getSpaceId: jest.fn(), }; return mocked; diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 75e56afcfd9bf..d8a42aa78d910 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -30,7 +30,7 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, - AlertInstanceSummary, + AlertSummary, AlertExecutionStatusValues, AlertNotifyWhenType, AlertTypeParams, @@ -68,7 +68,7 @@ import { SAVED_OBJECT_REL_PRIMARY, } from '../../../event_log/server'; import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; -import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log'; +import { alertSummaryFromEventLog } from '../lib/alert_summary_from_event_log'; import { AuditLogger } from '../../../security/server'; import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; @@ -193,7 +193,7 @@ export interface UpdateOptions { }; } -export interface GetAlertInstanceSummaryParams { +export interface GetAlertSummaryParams { id: string; dateStart?: string; } @@ -508,29 +508,26 @@ export class RulesClient { } } - public async getAlertInstanceSummary({ - id, - dateStart, - }: GetAlertInstanceSummaryParams): Promise { - this.logger.debug(`getAlertInstanceSummary(): getting alert ${id}`); - const alert = (await this.get({ id, includeLegacyId: true })) as SanitizedAlertWithLegacyId; + public async getAlertSummary({ id, dateStart }: GetAlertSummaryParams): Promise { + this.logger.debug(`getAlertSummary(): getting alert ${id}`); + const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedAlertWithLegacyId; await this.authorization.ensureAuthorized({ - ruleTypeId: alert.alertTypeId, - consumer: alert.consumer, + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, operation: ReadOperations.GetAlertSummary, entity: AlertingAuthorizationEntity.Rule, }); - // default duration of instance summary is 60 * alert interval + // default duration of instance summary is 60 * rule interval const dateNow = new Date(); - const durationMillis = parseDuration(alert.schedule.interval) * 60; + const durationMillis = parseDuration(rule.schedule.interval) * 60; const defaultDateStart = new Date(dateNow.valueOf() - durationMillis); const parsedDateStart = parseDate(dateStart, 'dateStart', defaultDateStart); const eventLogClient = await this.getEventLogClient(); - this.logger.debug(`getAlertInstanceSummary(): search the event log for alert ${id}`); + this.logger.debug(`getAlertSummary(): search the event log for rule ${id}`); let events: IEvent[]; try { const queryResults = await eventLogClient.findEventsBySavedObjectIds( @@ -543,18 +540,18 @@ export class RulesClient { end: dateNow.toISOString(), sort_order: 'desc', }, - alert.legacyId !== null ? [alert.legacyId] : undefined + rule.legacyId !== null ? [rule.legacyId] : undefined ); events = queryResults.data; } catch (err) { this.logger.debug( - `rulesClient.getAlertInstanceSummary(): error searching event log for alert ${id}: ${err.message}` + `rulesClient.getAlertSummary(): error searching event log for rule ${id}: ${err.message}` ); events = []; } - return alertInstanceSummaryFromEventLog({ - alert, + return alertSummaryFromEventLog({ + rule, events, dateStart: parsedDateStart.toISOString(), dateEnd: dateNow.toISOString(), diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts similarity index 71% rename from x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts rename to x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts index bd88b0f53ab07..9e34d2e027987 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts @@ -18,7 +18,7 @@ import { ActionsAuthorization } from '../../../../actions/server'; import { eventLogClientMock } from '../../../../event_log/server/mocks'; import { QueryEventsBySavedObjectResult } from '../../../../event_log/server'; import { SavedObject } from 'kibana/server'; -import { EventsFactory } from '../../lib/alert_instance_summary_from_event_log.test'; +import { EventsFactory } from '../../lib/alert_summary_from_event_log.test'; import { RawAlert } from '../../types'; import { getBeforeSetup, mockedDateString, setGlobalDate } from './lib'; @@ -55,26 +55,26 @@ beforeEach(() => { setGlobalDate(); -const AlertInstanceSummaryFindEventsResult: QueryEventsBySavedObjectResult = { +const AlertSummaryFindEventsResult: QueryEventsBySavedObjectResult = { page: 1, per_page: 10000, total: 0, data: [], }; -const AlertInstanceSummaryIntervalSeconds = 1; +const RuleIntervalSeconds = 1; -const BaseAlertInstanceSummarySavedObject: SavedObject = { +const BaseRuleSavedObject: SavedObject = { id: '1', type: 'alert', attributes: { enabled: true, - name: 'alert-name', + name: 'rule-name', tags: ['tag-1', 'tag-2'], alertTypeId: '123', - consumer: 'alert-consumer', + consumer: 'rule-consumer', legacyId: null, - schedule: { interval: `${AlertInstanceSummaryIntervalSeconds}s` }, + schedule: { interval: `${RuleIntervalSeconds}s` }, actions: [], params: {}, createdBy: null, @@ -96,16 +96,14 @@ const BaseAlertInstanceSummarySavedObject: SavedObject = { references: [], }; -function getAlertInstanceSummarySavedObject( - attributes: Partial = {} -): SavedObject { +function getRuleSavedObject(attributes: Partial = {}): SavedObject { return { - ...BaseAlertInstanceSummarySavedObject, - attributes: { ...BaseAlertInstanceSummarySavedObject.attributes, ...attributes }, + ...BaseRuleSavedObject, + attributes: { ...BaseRuleSavedObject.attributes, ...attributes }, }; } -describe('getAlertInstanceSummary()', () => { +describe('getAlertSummary()', () => { let rulesClient: RulesClient; beforeEach(() => { @@ -113,25 +111,25 @@ describe('getAlertInstanceSummary()', () => { }); test('runs as expected with some event log data', async () => { - const alertSO = getAlertInstanceSummarySavedObject({ - mutedInstanceIds: ['instance-muted-no-activity'], + const ruleSO = getRuleSavedObject({ + mutedInstanceIds: ['alert-muted-no-activity'], }); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(ruleSO); const eventsFactory = new EventsFactory(mockedDateString); const events = eventsFactory .addExecute() - .addNewInstance('instance-currently-active') - .addNewInstance('instance-previously-active') - .addActiveInstance('instance-currently-active', 'action group A') - .addActiveInstance('instance-previously-active', 'action group B') + .addNewAlert('alert-currently-active') + .addNewAlert('alert-previously-active') + .addActiveAlert('alert-currently-active', 'action group A') + .addActiveAlert('alert-previously-active', 'action group B') .advanceTime(10000) .addExecute() - .addRecoveredInstance('instance-previously-active') - .addActiveInstance('instance-currently-active', 'action group A') + .addRecoveredAlert('alert-previously-active') + .addActiveAlert('alert-currently-active', 'action group A') .getEvents(); const eventsResult = { - ...AlertInstanceSummaryFindEventsResult, + ...AlertSummaryFindEventsResult, total: events.length, data: events, }; @@ -141,31 +139,26 @@ describe('getAlertInstanceSummary()', () => { const durations: number[] = eventsFactory.getExecutionDurations(); - const result = await rulesClient.getAlertInstanceSummary({ id: '1', dateStart }); + const result = await rulesClient.getAlertSummary({ id: '1', dateStart }); const resultWithoutExecutionDuration = omit(result, 'executionDuration'); expect(resultWithoutExecutionDuration).toMatchInlineSnapshot(` Object { - "alertTypeId": "123", - "consumer": "alert-consumer", - "enabled": true, - "errorMessages": Array [], - "id": "1", - "instances": Object { - "instance-currently-active": Object { + "alerts": Object { + "alert-currently-active": Object { "actionGroupId": "action group A", "actionSubgroup": undefined, "activeStartDate": "2019-02-12T21:01:22.479Z", "muted": false, "status": "Active", }, - "instance-muted-no-activity": Object { + "alert-muted-no-activity": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, "muted": true, "status": "OK", }, - "instance-previously-active": Object { + "alert-previously-active": Object { "actionGroupId": undefined, "actionSubgroup": undefined, "activeStartDate": undefined, @@ -173,9 +166,14 @@ describe('getAlertInstanceSummary()', () => { "status": "OK", }, }, + "consumer": "rule-consumer", + "enabled": true, + "errorMessages": Array [], + "id": "1", "lastRun": "2019-02-12T21:01:32.479Z", "muteAll": false, - "name": "alert-name", + "name": "rule-name", + "ruleTypeId": "123", "status": "Active", "statusEndDate": "2019-02-12T21:01:22.479Z", "statusStartDate": "2019-02-12T21:00:22.479Z", @@ -193,18 +191,16 @@ describe('getAlertInstanceSummary()', () => { }); }); - // Further tests don't check the result of `getAlertInstanceSummary()`, as the result - // is just the result from the `alertInstanceSummaryFromEventLog()`, which itself + // Further tests don't check the result of `getAlertSummary()`, as the result + // is just the result from the `alertSummaryFromEventLog()`, which itself // has a complete set of tests. These tests just make sure the data gets - // sent into `getAlertInstanceSummary()` as appropriate. + // sent into `getAlertSummary()` as appropriate. test('calls saved objects and event log client with default params', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); - await rulesClient.getAlertInstanceSummary({ id: '1' }); + await rulesClient.getAlertSummary({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); @@ -230,20 +226,18 @@ describe('getAlertInstanceSummary()', () => { const startMillis = Date.parse(start!); const endMillis = Date.parse(end!); - const expectedDuration = 60 * AlertInstanceSummaryIntervalSeconds * 1000; + const expectedDuration = 60 * RuleIntervalSeconds * 1000; expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2); expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); }); test('calls event log client with legacy ids param', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce( - getAlertInstanceSummarySavedObject({ legacyId: '99999' }) - ); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult + getRuleSavedObject({ legacyId: '99999' }) ); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); - await rulesClient.getAlertInstanceSummary({ id: '1' }); + await rulesClient.getAlertSummary({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); @@ -268,15 +262,11 @@ describe('getAlertInstanceSummary()', () => { }); test('calls event log client with start date', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); - const dateStart = new Date( - Date.now() - 60 * AlertInstanceSummaryIntervalSeconds * 1000 - ).toISOString(); - await rulesClient.getAlertInstanceSummary({ id: '1', dateStart }); + const dateStart = new Date(Date.now() - 60 * RuleIntervalSeconds * 1000).toISOString(); + await rulesClient.getAlertSummary({ id: '1', dateStart }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); @@ -291,13 +281,11 @@ describe('getAlertInstanceSummary()', () => { }); test('calls event log client with relative start date', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); const dateStart = '2m'; - await rulesClient.getAlertInstanceSummary({ id: '1', dateStart }); + await rulesClient.getAlertSummary({ id: '1', dateStart }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); @@ -312,35 +300,27 @@ describe('getAlertInstanceSummary()', () => { }); test('invalid start date throws an error', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); const dateStart = 'ain"t no way this will get parsed as a date'; - expect( - rulesClient.getAlertInstanceSummary({ id: '1', dateStart }) - ).rejects.toMatchInlineSnapshot( + expect(rulesClient.getAlertSummary({ id: '1', dateStart })).rejects.toMatchInlineSnapshot( `[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]` ); }); test('saved object get throws an error', async () => { unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(AlertSummaryFindEventsResult); - expect(rulesClient.getAlertInstanceSummary({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: OMG!]` - ); + expect(rulesClient.getAlertSummary({ id: '1' })).rejects.toMatchInlineSnapshot(`[Error: OMG!]`); }); test('findEvents throws an error', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getRuleSavedObject()); eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('OMG 2!')); // error eaten but logged - await rulesClient.getAlertInstanceSummary({ id: '1' }); + await rulesClient.getAlertSummary({ id: '1' }); }); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 38bb3eb523d24..95bfc395e78f7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -24832,13 +24832,13 @@ "xpack.triggersActionsUI.sections.alertAddFooter.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertDetails.alertDetailsTitle": "{alertName}", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledRule": "このルールは無効になっていて再表示できません。[↑ を無効にする]を切り替えてアクティブにします。", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.alert": "アラート", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "期間", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.mute": "ミュート", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.start": "開始", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status": "ステータス", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "アクティブ", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "回復済み", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.alert": "アラート", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.duration": "期間", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.mute": "ミュート", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.start": "開始", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.status": "ステータス", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.status.active": "アクティブ", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.status.inactive": "回復済み", "xpack.triggersActionsUI.sections.alertDetails.alerts.disabledRuleTitle": "無効なルール", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableLoadingTitle": "有効にする", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle": "有効にする", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d5a2172e24104..921bd74939afa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25252,13 +25252,13 @@ "xpack.triggersActionsUI.sections.alertAddFooter.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertDetails.alertDetailsTitle": "{alertName}", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledRule": "此规则已禁用,无法显示。切换禁用 ↑ 以激活。", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.alert": "告警", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "持续时间", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.mute": "静音", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.start": "启动", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status": "状态", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "活动", - "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "已恢复", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.alert": "告警", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.duration": "持续时间", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.mute": "静音", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.start": "启动", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.status": "状态", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.status.active": "活动", + "xpack.triggersActionsUI.sections.alertDetails.alertsList.status.inactive": "已恢复", "xpack.triggersActionsUI.sections.alertDetails.alerts.disabledRuleTitle": "已禁用规则", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableLoadingTitle": "启用", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle": "启用", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts index 9f62e0ed0d50e..a590b4025e82a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.test.ts @@ -6,15 +6,15 @@ */ import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; -import { AlertInstanceSummary } from '../../../../../alerting/common'; -import { loadAlertInstanceSummary } from './alert_summary'; +import { AlertSummary } from '../../../../../alerting/common'; +import { loadAlertSummary } from './alert_summary'; const http = httpServiceMock.createStartContract(); -describe('loadAlertInstanceSummary', () => { - test('should call get alert types API', async () => { - const resolvedValue: AlertInstanceSummary = { - instances: {}, +describe('loadAlertSummary', () => { + test('should call alert summary API', async () => { + const resolvedValue: AlertSummary = { + alerts: {}, consumer: 'alerts', enabled: true, errorMessages: [], @@ -22,7 +22,7 @@ describe('loadAlertInstanceSummary', () => { lastRun: '2021-04-01T22:18:27.609Z', muteAll: false, name: 'test', - alertTypeId: '.index-threshold', + ruleTypeId: '.index-threshold', status: 'OK', statusEndDate: '2021-04-01T22:19:25.174Z', statusStartDate: '2021-04-01T21:19:25.174Z', @@ -55,7 +55,7 @@ describe('loadAlertInstanceSummary', () => { }, }); - const result = await loadAlertInstanceSummary({ http, alertId: 'te/st' }); + const result = await loadAlertSummary({ http, ruleId: 'te/st' }); expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts index 35a757f4b6afe..9a4109ae1ff37 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/alert_summary.ts @@ -6,12 +6,11 @@ */ import { HttpSetup } from 'kibana/public'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -import { AlertInstanceSummary } from '../../../types'; +import { AlertSummary } from '../../../types'; import { RewriteRequestCase, AsApiContract } from '../../../../../actions/common'; -const rewriteBodyRes: RewriteRequestCase = ({ - alerts, - rule_type_id: alertTypeId, +const rewriteBodyRes: RewriteRequestCase = ({ + rule_type_id: ruleTypeId, mute_all: muteAll, status_start_date: statusStartDate, status_end_date: statusEndDate, @@ -21,25 +20,24 @@ const rewriteBodyRes: RewriteRequestCase = ({ ...rest }: any) => ({ ...rest, - alertTypeId, + ruleTypeId, muteAll, statusStartDate, statusEndDate, errorMessages, lastRun, - instances: alerts, executionDuration, }); -export async function loadAlertInstanceSummary({ +export async function loadAlertSummary({ http, - alertId, + ruleId, }: { http: HttpSetup; - alertId: string; -}): Promise { - const res = await http.get>( - `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(alertId)}/_alert_summary` + ruleId: string; +}): Promise { + const res = await http.get>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(ruleId)}/_alert_summary` ); return rewriteBodyRes(res); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts index c499f7955e2fe..ea4f146cb6acb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts @@ -13,7 +13,7 @@ export { deleteAlerts } from './delete'; export { disableAlert, disableAlerts } from './disable'; export { enableAlert, enableAlerts } from './enable'; export { loadAlert } from './get_rule'; -export { loadAlertInstanceSummary } from './alert_summary'; +export { loadAlertSummary } from './alert_summary'; export { muteAlertInstance } from './mute_alert'; export { muteAlert, muteAlerts } from './mute'; export { loadAlertTypes } from './rule_types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 3b15295cf7a3a..93aaec8b13650 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -36,7 +36,7 @@ import { ComponentOpts as BulkOperationsComponentOpts, withBulkAlertOperations, } from '../../common/components/with_bulk_alert_api_operations'; -import { AlertInstancesRouteWithApi } from './alert_instances_route'; +import { AlertsRouteWithApi } from './alerts_route'; import { ViewInApp } from './view_in_app'; import { AlertEdit } from '../../alert_form'; import { routeToRuleDetails } from '../../../constants'; @@ -441,10 +441,10 @@ export const AlertDetails: React.FunctionComponent = ({ {alert.enabled ? ( - ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.scss similarity index 87% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss rename to x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.scss index 454d6e20d9323..243ba5bc9c52c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.scss @@ -1,6 +1,6 @@ // Add truncation to heath status -.actionsInstanceList__health { +.alertsList__health { width: 100%; .euiFlexItem:last-of-type { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx similarity index 61% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx index a790427e36483..c851f0f273064 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.test.tsx @@ -10,8 +10,8 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { act } from 'react-dom/test-utils'; -import { AlertInstances, AlertInstanceListItem, alertInstanceToListItem } from './alert_instances'; -import { Alert, AlertInstanceSummary, AlertInstanceStatus, AlertType } from '../../../../types'; +import { Alerts, AlertListItem, alertToListItem } from './alerts'; +import { Alert, AlertSummary, AlertStatus, AlertType } from '../../../../types'; import { EuiBasicTable } from '@elastic/eui'; import { ExecutionDurationChart } from '../../common/components/execution_duration_chart'; @@ -31,18 +31,18 @@ beforeAll(() => { global.Date.now = jest.fn(() => fakeNow.getTime()); }); -describe('alert_instances', () => { - it('render a list of alert instances', () => { - const alert = mockAlert(); - const alertType = mockAlertType(); - const alertInstanceSummary = mockAlertInstanceSummary({ - instances: { - first_instance: { +describe('alerts', () => { + it('render a list of alerts', () => { + const rule = mockRule(); + const ruleType = mockRuleType(); + const alertSummary = mockAlertSummary({ + alerts: { + first_alert: { status: 'OK', muted: false, actionGroupId: 'default', }, - second_instance: { + second_alert: { status: 'Active', muted: false, actionGroupId: 'action group id unknown', @@ -50,63 +50,58 @@ describe('alert_instances', () => { }, }); - const instances: AlertInstanceListItem[] = [ + const alerts: AlertListItem[] = [ // active first - alertInstanceToListItem( + alertToListItem( fakeNow.getTime(), - alertType, - 'second_instance', - alertInstanceSummary.instances.second_instance + ruleType, + 'second_alert', + alertSummary.alerts.second_alert ), // ok second - alertInstanceToListItem( - fakeNow.getTime(), - alertType, - 'first_instance', - alertInstanceSummary.instances.first_instance - ), + alertToListItem(fakeNow.getTime(), ruleType, 'first_alert', alertSummary.alerts.first_alert), ]; expect( shallow( - ) .find(EuiBasicTable) .prop('items') - ).toEqual(instances); + ).toEqual(alerts); }); it('render a hidden field with duration epoch', () => { - const alert = mockAlert(); - const alertType = mockAlertType(); - const alertInstanceSummary = mockAlertInstanceSummary(); + const rule = mockRule(); + const ruleType = mockRuleType(); + const alertSummary = mockAlertSummary(); expect( shallow( - ) - .find('[name="alertInstancesDurationEpoch"]') + .find('[name="alertsDurationEpoch"]') .prop('value') ).toEqual(fake2MinutesAgo.getTime()); }); - it('render all active alert instances', () => { - const alert = mockAlert(); - const alertType = mockAlertType(); - const instances: Record = { + it('render all active alerts', () => { + const rule = mockRule(); + const ruleType = mockRuleType(); + const alerts: Record = { ['us-central']: { status: 'OK', muted: false, @@ -118,41 +113,41 @@ describe('alert_instances', () => { }; expect( shallow( - ) .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-central', instances['us-central']), - alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-east', instances['us-east']), + alertToListItem(fakeNow.getTime(), ruleType, 'us-central', alerts['us-central']), + alertToListItem(fakeNow.getTime(), ruleType, 'us-east', alerts['us-east']), ]); }); - it('render all inactive alert instances', () => { - const alert = mockAlert({ + it('render all inactive alerts', () => { + const rule = mockRule({ mutedInstanceIds: ['us-west', 'us-east'], }); - const alertType = mockAlertType(); - const instanceUsWest: AlertInstanceStatus = { status: 'OK', muted: false }; - const instanceUsEast: AlertInstanceStatus = { status: 'OK', muted: false }; + const ruleType = mockRuleType(); + const alertUsWest: AlertStatus = { status: 'OK', muted: false }; + const alertUsEast: AlertStatus = { status: 'OK', muted: false }; expect( shallow( - { .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-west', instanceUsWest), - alertInstanceToListItem(fakeNow.getTime(), alertType, 'us-east', instanceUsEast), + alertToListItem(fakeNow.getTime(), ruleType, 'us-west', alertUsWest), + alertToListItem(fakeNow.getTime(), ruleType, 'us-east', alertUsEast), ]); }); }); -describe('alertInstanceToListItem', () => { - it('handles active instances', () => { - const alertType = mockAlertType({ +describe('alertToListItem', () => { + it('handles active alerts', () => { + const ruleType = mockRuleType({ actionGroups: [ { id: 'default', name: 'Default Action Group' }, { id: 'testing', name: 'Test Action Group' }, ], }); const start = fake2MinutesAgo; - const instance: AlertInstanceStatus = { + const alert: AlertStatus = { status: 'Active', muted: false, activeStartDate: fake2MinutesAgo.toISOString(), actionGroupId: 'testing', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ - instance: 'id', + expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({ + alert: 'id', status: { label: 'Active', actionGroup: 'Test Action Group', healthColor: 'primary' }, start, sortPriority: 0, @@ -200,17 +195,17 @@ describe('alertInstanceToListItem', () => { }); }); - it('handles active instances with no action group id', () => { - const alertType = mockAlertType(); + it('handles active alerts with no action group id', () => { + const ruleType = mockRuleType(); const start = fake2MinutesAgo; - const instance: AlertInstanceStatus = { + const alert: AlertStatus = { status: 'Active', muted: false, activeStartDate: fake2MinutesAgo.toISOString(), }; - expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ - instance: 'id', + expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({ + alert: 'id', status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, start, sortPriority: 0, @@ -219,18 +214,18 @@ describe('alertInstanceToListItem', () => { }); }); - it('handles active muted instances', () => { - const alertType = mockAlertType(); + it('handles active muted alerts', () => { + const ruleType = mockRuleType(); const start = fake2MinutesAgo; - const instance: AlertInstanceStatus = { + const alert: AlertStatus = { status: 'Active', muted: true, activeStartDate: fake2MinutesAgo.toISOString(), actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ - instance: 'id', + expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({ + alert: 'id', status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, start, sortPriority: 0, @@ -239,16 +234,16 @@ describe('alertInstanceToListItem', () => { }); }); - it('handles active instances with start date', () => { - const alertType = mockAlertType(); - const instance: AlertInstanceStatus = { + it('handles active alerts with start date', () => { + const ruleType = mockRuleType(); + const alert: AlertStatus = { status: 'Active', muted: false, actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ - instance: 'id', + expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({ + alert: 'id', status: { label: 'Active', actionGroup: 'Default Action Group', healthColor: 'primary' }, start: undefined, duration: 0, @@ -257,15 +252,15 @@ describe('alertInstanceToListItem', () => { }); }); - it('handles muted inactive instances', () => { - const alertType = mockAlertType(); - const instance: AlertInstanceStatus = { + it('handles muted inactive alerts', () => { + const ruleType = mockRuleType(); + const alert: AlertStatus = { status: 'OK', muted: true, actionGroupId: 'default', }; - expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ - instance: 'id', + expect(alertToListItem(fakeNow.getTime(), ruleType, 'id', alert)).toEqual({ + alert: 'id', status: { label: 'Recovered', healthColor: 'subdued' }, start: undefined, duration: 0, @@ -277,19 +272,19 @@ describe('alertInstanceToListItem', () => { describe('execution duration overview', () => { it('render last execution status', async () => { - const rule = mockAlert({ + const rule = mockRule({ executionStatus: { status: 'ok', lastExecutionDate: new Date('2020-08-20T19:23:38Z') }, }); - const ruleType = mockAlertType(); - const alertSummary = mockAlertInstanceSummary(); + const ruleType = mockRuleType(); + const alertSummary = mockAlertSummary(); const wrapper = mountWithIntl( - ); @@ -305,19 +300,19 @@ describe('execution duration overview', () => { }); it('renders average execution duration', async () => { - const rule = mockAlert(); - const ruleType = mockAlertType({ ruleTaskTimeout: '10m' }); - const alertSummary = mockAlertInstanceSummary({ + const rule = mockRule(); + const ruleType = mockRuleType({ ruleTaskTimeout: '10m' }); + const alertSummary = mockAlertSummary({ executionDuration: { average: 60284, values: [] }, }); const wrapper = mountWithIntl( - ); @@ -336,19 +331,19 @@ describe('execution duration overview', () => { }); it('renders warning when average execution duration exceeds rule timeout', async () => { - const rule = mockAlert(); - const ruleType = mockAlertType({ ruleTaskTimeout: '10m' }); - const alertSummary = mockAlertInstanceSummary({ + const rule = mockRule(); + const ruleType = mockRuleType({ ruleTaskTimeout: '10m' }); + const alertSummary = mockAlertSummary({ executionDuration: { average: 60284345, values: [] }, }); const wrapper = mountWithIntl( - ); @@ -367,17 +362,17 @@ describe('execution duration overview', () => { }); it('renders execution duration chart', () => { - const rule = mockAlert(); - const ruleType = mockAlertType(); - const alertSummary = mockAlertInstanceSummary(); + const rule = mockRule(); + const ruleType = mockRuleType(); + const alertSummary = mockAlertSummary(); expect( shallow( - ) @@ -387,11 +382,11 @@ describe('execution duration overview', () => { }); }); -function mockAlert(overloads: Partial = {}): Alert { +function mockRule(overloads: Partial = {}): Alert { return { id: uuid.v4(), enabled: true, - name: `alert-${uuid.v4()}`, + name: `rule-${uuid.v4()}`, tags: [], alertTypeId: '.noop', consumer: 'consumer', @@ -415,10 +410,10 @@ function mockAlert(overloads: Partial = {}): Alert { }; } -function mockAlertType(overloads: Partial = {}): AlertType { +function mockRuleType(overloads: Partial = {}): AlertType { return { - id: 'test.testAlertType', - name: 'My Test Alert Type', + id: 'test.testRuleType', + name: 'My Test Rule Type', actionGroups: [{ id: 'default', name: 'Default Action Group' }], actionVariables: { context: [], @@ -435,15 +430,13 @@ function mockAlertType(overloads: Partial = {}): AlertType { }; } -function mockAlertInstanceSummary( - overloads: Partial = {} -): AlertInstanceSummary { - const summary: AlertInstanceSummary = { - id: 'alert-id', - name: 'alert-name', +function mockAlertSummary(overloads: Partial = {}): AlertSummary { + const summary: AlertSummary = { + id: 'rule-id', + name: 'rule-name', tags: ['tag-1', 'tag-2'], - alertTypeId: 'alert-type-id', - consumer: 'alert-consumer', + ruleTypeId: 'rule-type-id', + consumer: 'rule-consumer', status: 'OK', muteAll: false, throttle: '', @@ -451,7 +444,7 @@ function mockAlertInstanceSummary( errorMessages: [], statusStartDate: fake2MinutesAgo.toISOString(), statusEndDate: fakeNow.toISOString(), - instances: { + alerts: { foo: { status: 'OK', muted: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx similarity index 59% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx index 9de70699edbdb..b0d9a504cc773 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts.tsx @@ -26,21 +26,15 @@ import { padStart, chunk } from 'lodash'; import { ActionGroup, AlertExecutionStatusErrorReasons, - AlertInstanceStatusValues, + AlertStatusValues, } from '../../../../../../alerting/common'; -import { - Alert, - AlertInstanceSummary, - AlertInstanceStatus, - AlertType, - Pagination, -} from '../../../../types'; +import { Alert, AlertSummary, AlertStatus, AlertType, Pagination } from '../../../../types'; import { ComponentOpts as AlertApis, withBulkAlertOperations, } from '../../common/components/with_bulk_alert_api_operations'; import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; -import './alert_instances.scss'; +import './alerts.scss'; import { RuleMutedSwitch } from './rule_muted_switch'; import { getHealthColor } from '../../alerts_list/components/alert_status_filter'; import { @@ -53,29 +47,28 @@ import { } from '../../../lib/execution_duration_utils'; import { ExecutionDurationChart } from '../../common/components/execution_duration_chart'; -type AlertInstancesProps = { - alert: Alert; - alertType: AlertType; +type AlertsProps = { + rule: Alert; + ruleType: AlertType; readOnly: boolean; - alertInstanceSummary: AlertInstanceSummary; + alertSummary: AlertSummary; requestRefresh: () => Promise; durationEpoch?: number; } & Pick; -export const alertInstancesTableColumns = ( - onMuteAction: (instance: AlertInstanceListItem) => Promise, +export const alertsTableColumns = ( + onMuteAction: (alert: AlertListItem) => Promise, readOnly: boolean ) => [ { - field: 'instance', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.alert', - { defaultMessage: 'Alert' } - ), + field: 'alert', + name: i18n.translate('xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.alert', { + defaultMessage: 'Alert', + }), sortable: false, truncateText: true, width: '45%', - 'data-test-subj': 'alertInstancesTableCell-instance', + 'data-test-subj': 'alertsTableCell-alert', render: (value: string) => { return ( @@ -87,66 +80,64 @@ export const alertInstancesTableColumns = ( { field: 'status', name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status', + 'xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.status', { defaultMessage: 'Status' } ), width: '15%', - render: (value: AlertInstanceListItemStatus, instance: AlertInstanceListItem) => { + render: (value: AlertListItemStatus) => { return ( - + {value.label} {value.actionGroup ? ` (${value.actionGroup})` : ``} ); }, sortable: false, - 'data-test-subj': 'alertInstancesTableCell-status', + 'data-test-subj': 'alertsTableCell-status', }, { field: 'start', width: '190px', - render: (value: Date | undefined, instance: AlertInstanceListItem) => { + render: (value: Date | undefined) => { return value ? moment(value).format('D MMM YYYY @ HH:mm:ss') : ''; }, - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.start', - { defaultMessage: 'Start' } - ), + name: i18n.translate('xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.start', { + defaultMessage: 'Start', + }), sortable: false, - 'data-test-subj': 'alertInstancesTableCell-start', + 'data-test-subj': 'alertsTableCell-start', }, { field: 'duration', - render: (value: number, instance: AlertInstanceListItem) => { + render: (value: number) => { return value ? durationAsString(moment.duration(value)) : ''; }, name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration', + 'xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.duration', { defaultMessage: 'Duration' } ), sortable: false, width: '80px', - 'data-test-subj': 'alertInstancesTableCell-duration', + 'data-test-subj': 'alertsTableCell-duration', }, { field: '', align: RIGHT_ALIGNMENT, width: '60px', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.mute', - { defaultMessage: 'Mute' } - ), - render: (alertInstance: AlertInstanceListItem) => { + name: i18n.translate('xpack.triggersActionsUI.sections.alertDetails.alertsList.columns.mute', { + defaultMessage: 'Mute', + }), + render: (alert: AlertListItem) => { return ( await onMuteAction(alertInstance)} - alertInstance={alertInstance} + onMuteAction={async () => await onMuteAction(alert)} + alert={alert} /> ); }, sortable: false, - 'data-test-subj': 'alertInstancesTableCell-actions', + 'data-test-subj': 'alertsTableCell-actions', }, ]; @@ -156,47 +147,45 @@ function durationAsString(duration: Duration): string { .join(':'); } -export function AlertInstances({ - alert, - alertType, +export function Alerts({ + rule, + ruleType, readOnly, - alertInstanceSummary, + alertSummary, muteAlertInstance, unmuteAlertInstance, requestRefresh, durationEpoch = Date.now(), -}: AlertInstancesProps) { +}: AlertsProps) { const [pagination, setPagination] = useState({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE, }); - const alertInstances = Object.entries(alertInstanceSummary.instances) - .map(([instanceId, instance]) => - alertInstanceToListItem(durationEpoch, alertType, instanceId, instance) - ) - .sort((leftInstance, rightInstance) => leftInstance.sortPriority - rightInstance.sortPriority); + const alerts = Object.entries(alertSummary.alerts) + .map(([alertId, alert]) => alertToListItem(durationEpoch, ruleType, alertId, alert)) + .sort((leftAlert, rightAlert) => leftAlert.sortPriority - rightAlert.sortPriority); - const pageOfAlertInstances = getPage(alertInstances, pagination); + const pageOfAlerts = getPage(alerts, pagination); - const onMuteAction = async (instance: AlertInstanceListItem) => { - await (instance.isMuted - ? unmuteAlertInstance(alert, instance.instance) - : muteAlertInstance(alert, instance.instance)); + const onMuteAction = async (alert: AlertListItem) => { + await (alert.isMuted + ? unmuteAlertInstance(rule, alert.alert) + : muteAlertInstance(rule, alert.alert)); requestRefresh(); }; const showDurationWarning = shouldShowDurationWarning( - alertType, - alertInstanceSummary.executionDuration.average + ruleType, + alertSummary.executionDuration.average ); - const healthColor = getHealthColor(alert.executionStatus.status); + const healthColor = getHealthColor(rule.executionStatus.status); const isLicenseError = - alert.executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License; + rule.executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License; const statusMessage = isLicenseError ? ALERT_STATUS_LICENSE_ERROR - : alertsStatusesTranslationsMapping[alert.executionStatus.status]; + : alertsStatusesTranslationsMapping[rule.executionStatus.status]; return ( <> @@ -205,11 +194,11 @@ export function AlertInstances({ @@ -217,7 +206,7 @@ export function AlertInstances({ } description={i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.ruleLastExecutionDescription', + 'xpack.triggersActionsUI.sections.alertDetails.alertsList.ruleLastExecutionDescription', { defaultMessage: `Last response`, } @@ -244,7 +233,7 @@ export function AlertInstances({ type="alert" color="warning" content={i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.ruleTypeExcessDurationMessage', + 'xpack.triggersActionsUI.sections.alertDetails.alertsList.ruleTypeExcessDurationMessage', { defaultMessage: `Duration exceeds the rule's expected run time.`, } @@ -254,12 +243,12 @@ export function AlertInstances({ )} - {formatMillisForDisplay(alertInstanceSummary.executionDuration.average)} + {formatMillisForDisplay(alertSummary.executionDuration.average)} } description={i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.avgDurationDescription', + 'xpack.triggersActionsUI.sections.alertDetails.alertsList.avgDurationDescription', { defaultMessage: `Average duration`, } @@ -268,54 +257,54 @@ export function AlertInstances({ - + { setPagination(changedPage); }} rowProps={() => ({ - 'data-test-subj': 'alert-instance-row', + 'data-test-subj': 'alert-row', })} cellProps={() => ({ 'data-test-subj': 'cell', })} - columns={alertInstancesTableColumns(onMuteAction, readOnly)} - data-test-subj="alertInstancesList" + columns={alertsTableColumns(onMuteAction, readOnly)} + data-test-subj="alertsList" tableLayout="fixed" - className="alertInstancesList" + className="alertsList" /> ); } -export const AlertInstancesWithApi = withBulkAlertOperations(AlertInstances); +export const AlertsWithApi = withBulkAlertOperations(Alerts); function getPage(items: any[], pagination: Pagination) { return chunk(items, pagination.size)[pagination.index] || []; } -interface AlertInstanceListItemStatus { +interface AlertListItemStatus { label: string; healthColor: string; actionGroup?: string; } -export interface AlertInstanceListItem { - instance: string; - status: AlertInstanceListItemStatus; +export interface AlertListItem { + alert: string; + status: AlertListItemStatus; start?: Date; duration: number; isMuted: boolean; @@ -323,43 +312,43 @@ export interface AlertInstanceListItem { } const ACTIVE_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active', + 'xpack.triggersActionsUI.sections.alertDetails.alertsList.status.active', { defaultMessage: 'Active' } ); const INACTIVE_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive', + 'xpack.triggersActionsUI.sections.alertDetails.alertsList.status.inactive', { defaultMessage: 'Recovered' } ); -function getActionGroupName(alertType: AlertType, actionGroupId?: string): string | undefined { - actionGroupId = actionGroupId || alertType.defaultActionGroupId; - const actionGroup = alertType?.actionGroups?.find( +function getActionGroupName(ruleType: AlertType, actionGroupId?: string): string | undefined { + actionGroupId = actionGroupId || ruleType.defaultActionGroupId; + const actionGroup = ruleType?.actionGroups?.find( (group: ActionGroup) => group.id === actionGroupId ); return actionGroup?.name; } -export function alertInstanceToListItem( +export function alertToListItem( durationEpoch: number, - alertType: AlertType, - instanceId: string, - instance: AlertInstanceStatus -): AlertInstanceListItem { - const isMuted = !!instance?.muted; + ruleType: AlertType, + alertId: string, + alert: AlertStatus +): AlertListItem { + const isMuted = !!alert?.muted; const status = - instance?.status === 'Active' + alert?.status === 'Active' ? { label: ACTIVE_LABEL, - actionGroup: getActionGroupName(alertType, instance?.actionGroupId), + actionGroup: getActionGroupName(ruleType, alert?.actionGroupId), healthColor: 'primary', } : { label: INACTIVE_LABEL, healthColor: 'subdued' }; - const start = instance?.activeStartDate ? new Date(instance.activeStartDate) : undefined; + const start = alert?.activeStartDate ? new Date(alert.activeStartDate) : undefined; const duration = start ? durationEpoch - start.valueOf() : 0; - const sortPriority = getSortPriorityByStatus(instance?.status); + const sortPriority = getSortPriorityByStatus(alert?.status); return { - instance: instanceId, + alert: alertId, status, start, duration, @@ -368,7 +357,7 @@ export function alertInstanceToListItem( }; } -function getSortPriorityByStatus(status?: AlertInstanceStatusValues): number { +function getSortPriorityByStatus(status?: AlertStatusValues): number { switch (status) { case 'Active': return 0; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx similarity index 59% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx index a0eb3212f7742..032539aadc702 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.test.tsx @@ -9,22 +9,22 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { ToastsApi } from 'kibana/public'; -import { AlertInstancesRoute, getAlertInstanceSummary } from './alert_instances_route'; -import { Alert, AlertInstanceSummary, AlertType } from '../../../../types'; +import { AlertsRoute, getAlertSummary } from './alerts_route'; +import { Alert, AlertSummary, AlertType } from '../../../../types'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; jest.mock('../../../../common/lib/kibana'); const fakeNow = new Date('2020-02-09T23:15:41.941Z'); const fake2MinutesAgo = new Date('2020-02-09T23:13:41.941Z'); -describe('alert_instance_summary_route', () => { +describe('alerts_summary_route', () => { it('render a loader while fetching data', () => { - const alert = mockAlert(); - const alertType = mockAlertType(); + const rule = mockRule(); + const ruleType = mockRuleType(); expect( shallow( - + ).containsMatchingElement() ).toBeTruthy(); }); @@ -35,62 +35,52 @@ describe('getAlertState useEffect handler', () => { jest.clearAllMocks(); }); - it('fetches alert instance summary', async () => { - const alert = mockAlert(); - const alertInstanceSummary = mockAlertInstanceSummary(); - const { loadAlertInstanceSummary } = mockApis(); - const { setAlertInstanceSummary } = mockStateSetter(); + it('fetches alert summary', async () => { + const rule = mockRule(); + const alertSummary = mockAlertSummary(); + const { loadAlertSummary } = mockApis(); + const { setAlertSummary } = mockStateSetter(); - loadAlertInstanceSummary.mockImplementationOnce(async () => alertInstanceSummary); + loadAlertSummary.mockImplementationOnce(async () => alertSummary); const toastNotifications = { addDanger: jest.fn(), } as unknown as ToastsApi; - await getAlertInstanceSummary( - alert.id, - loadAlertInstanceSummary, - setAlertInstanceSummary, - toastNotifications - ); + await getAlertSummary(rule.id, loadAlertSummary, setAlertSummary, toastNotifications); - expect(loadAlertInstanceSummary).toHaveBeenCalledWith(alert.id); - expect(setAlertInstanceSummary).toHaveBeenCalledWith(alertInstanceSummary); + expect(loadAlertSummary).toHaveBeenCalledWith(rule.id); + expect(setAlertSummary).toHaveBeenCalledWith(alertSummary); }); - it('displays an error if the alert instance summary isnt found', async () => { - const actionType = { + it('displays an error if the alert summary isnt found', async () => { + const connectorType = { id: '.server-log', name: 'Server log', enabled: true, }; - const alert = mockAlert({ + const rule = mockRule({ actions: [ { group: '', id: uuid.v4(), - actionTypeId: actionType.id, + actionTypeId: connectorType.id, params: {}, }, ], }); - const { loadAlertInstanceSummary } = mockApis(); - const { setAlertInstanceSummary } = mockStateSetter(); + const { loadAlertSummary } = mockApis(); + const { setAlertSummary } = mockStateSetter(); - loadAlertInstanceSummary.mockImplementation(async () => { + loadAlertSummary.mockImplementation(async () => { throw new Error('OMG'); }); const toastNotifications = { addDanger: jest.fn(), } as unknown as ToastsApi; - await getAlertInstanceSummary( - alert.id, - loadAlertInstanceSummary, - setAlertInstanceSummary, - toastNotifications - ); + await getAlertSummary(rule.id, loadAlertSummary, setAlertSummary, toastNotifications); expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); expect(toastNotifications.addDanger).toHaveBeenCalledWith({ title: 'Unable to load alerts: OMG', @@ -100,22 +90,22 @@ describe('getAlertState useEffect handler', () => { function mockApis() { return { - loadAlertInstanceSummary: jest.fn(), + loadAlertSummary: jest.fn(), requestRefresh: jest.fn(), }; } function mockStateSetter() { return { - setAlertInstanceSummary: jest.fn(), + setAlertSummary: jest.fn(), }; } -function mockAlert(overloads: Partial = {}): Alert { +function mockRule(overloads: Partial = {}): Alert { return { id: uuid.v4(), enabled: true, - name: `alert-${uuid.v4()}`, + name: `rule-${uuid.v4()}`, tags: [], alertTypeId: '.noop', consumer: 'consumer', @@ -139,10 +129,10 @@ function mockAlert(overloads: Partial = {}): Alert { }; } -function mockAlertType(overloads: Partial = {}): AlertType { +function mockRuleType(overloads: Partial = {}): AlertType { return { - id: 'test.testAlertType', - name: 'My Test Alert Type', + id: 'test.testRuleType', + name: 'My Test Rule Type', actionGroups: [{ id: 'default', name: 'Default Action Group' }], actionVariables: { context: [], @@ -159,13 +149,13 @@ function mockAlertType(overloads: Partial = {}): AlertType { }; } -function mockAlertInstanceSummary(overloads: Partial = {}): any { - const summary: AlertInstanceSummary = { - id: 'alert-id', - name: 'alert-name', +function mockAlertSummary(overloads: Partial = {}): any { + const summary: AlertSummary = { + id: 'rule-id', + name: 'rule-name', tags: ['tag-1', 'tag-2'], - alertTypeId: 'alert-type-id', - consumer: 'alert-consumer', + ruleTypeId: 'rule-type-id', + consumer: 'rule-consumer', status: 'OK', muteAll: false, throttle: null, @@ -173,7 +163,7 @@ function mockAlertInstanceSummary(overloads: Partial = {}): any { errorMessages: [], statusStartDate: fake2MinutesAgo.toISOString(), statusEndDate: fakeNow.toISOString(), - instances: { + alerts: { foo: { status: 'OK', muted: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx similarity index 52% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx index 713e8e8b6cc95..e99df3dcbd233 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alerts_route.tsx @@ -8,64 +8,62 @@ import { i18n } from '@kbn/i18n'; import { ToastsApi } from 'kibana/public'; import React, { useState, useEffect } from 'react'; -import { Alert, AlertInstanceSummary, AlertType } from '../../../../types'; +import { Alert, AlertSummary, AlertType } from '../../../../types'; import { ComponentOpts as AlertApis, withBulkAlertOperations, } from '../../common/components/with_bulk_alert_api_operations'; -import { AlertInstancesWithApi as AlertInstances } from './alert_instances'; +import { AlertsWithApi as Alerts } from './alerts'; import { useKibana } from '../../../../common/lib/kibana'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; -type WithAlertInstanceSummaryProps = { - alert: Alert; - alertType: AlertType; +type WithAlertSummaryProps = { + rule: Alert; + ruleType: AlertType; readOnly: boolean; requestRefresh: () => Promise; -} & Pick; +} & Pick; -export const AlertInstancesRoute: React.FunctionComponent = ({ - alert, - alertType, +export const AlertsRoute: React.FunctionComponent = ({ + rule, + ruleType, readOnly, requestRefresh, - loadAlertInstanceSummary: loadAlertInstanceSummary, + loadAlertSummary: loadAlertSummary, }) => { const { notifications: { toasts }, } = useKibana().services; - const [alertInstanceSummary, setAlertInstanceSummary] = useState( - null - ); + const [alertSummary, setAlertSummary] = useState(null); useEffect(() => { - getAlertInstanceSummary(alert.id, loadAlertInstanceSummary, setAlertInstanceSummary, toasts); + getAlertSummary(rule.id, loadAlertSummary, setAlertSummary, toasts); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [alert]); + }, [rule]); - return alertInstanceSummary ? ( - ) : ( ); }; -export async function getAlertInstanceSummary( - alertId: string, - loadAlertInstanceSummary: AlertApis['loadAlertInstanceSummary'], - setAlertInstanceSummary: React.Dispatch>, +export async function getAlertSummary( + ruleId: string, + loadAlertSummary: AlertApis['loadAlertSummary'], + setAlertSummary: React.Dispatch>, toasts: Pick ) { try { - const loadedInstanceSummary = await loadAlertInstanceSummary(alertId); - setAlertInstanceSummary(loadedInstanceSummary); + const loadedSummary = await loadAlertSummary(ruleId); + setAlertSummary(loadedSummary); } catch (e) { toasts.addDanger({ title: i18n.translate( @@ -81,4 +79,4 @@ export async function getAlertInstanceSummary( } } -export const AlertInstancesRouteWithApi = withBulkAlertOperations(AlertInstancesRoute); +export const AlertsRouteWithApi = withBulkAlertOperations(AlertsRoute); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx index bee0c8aef706d..ce550243bcc37 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx @@ -8,20 +8,20 @@ import React, { useState } from 'react'; import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui'; -import { AlertInstanceListItem } from './alert_instances'; +import { AlertListItem } from './alerts'; interface ComponentOpts { - alertInstance: AlertInstanceListItem; - onMuteAction: (instance: AlertInstanceListItem) => Promise; + alert: AlertListItem; + onMuteAction: (instance: AlertListItem) => Promise; disabled: boolean; } export const RuleMutedSwitch: React.FunctionComponent = ({ - alertInstance, + alert, onMuteAction, disabled, }: ComponentOpts) => { - const [isMuted, setIsMuted] = useState(alertInstance?.isMuted); + const [isMuted, setIsMuted] = useState(alert?.isMuted); const [isUpdating, setIsUpdating] = useState(false); return isUpdating ? ( @@ -34,11 +34,11 @@ export const RuleMutedSwitch: React.FunctionComponent = ({ checked={isMuted} onChange={async () => { setIsUpdating(true); - await onMuteAction(alertInstance); + await onMuteAction(alert); setIsMuted(!isMuted); setIsUpdating(false); }} - data-test-subj={`muteAlertInstanceButton_${alertInstance.instance}`} + data-test-subj={`muteAlertButton_${alert.alert}`} showLabel={false} label="mute" /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx index 0f3119b58e073..6f4d5b2ee7531 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -11,7 +11,7 @@ import { Alert, AlertType, AlertTaskState, - AlertInstanceSummary, + AlertSummary, AlertingFrameworkHealth, ResolvedRule, } from '../../../../types'; @@ -29,7 +29,7 @@ import { unmuteAlertInstance, loadAlert, loadAlertState, - loadAlertInstanceSummary, + loadAlertSummary, loadAlertTypes, alertingFrameworkHealth, resolveRule, @@ -57,7 +57,7 @@ export interface ComponentOpts { }>; loadAlert: (id: Alert['id']) => Promise; loadAlertState: (id: Alert['id']) => Promise; - loadAlertInstanceSummary: (id: Alert['id']) => Promise; + loadAlertSummary: (id: Alert['id']) => Promise; loadAlertTypes: () => Promise; getHealth: () => Promise; resolveRule: (id: Alert['id']) => Promise; @@ -127,9 +127,7 @@ export function withBulkAlertOperations( deleteAlert={async (alert: Alert) => deleteAlerts({ http, ids: [alert.id] })} loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })} loadAlertState={async (alertId: Alert['id']) => loadAlertState({ http, alertId })} - loadAlertInstanceSummary={async (alertId: Alert['id']) => - loadAlertInstanceSummary({ http, alertId }) - } + loadAlertSummary={async (ruleId: Alert['id']) => loadAlertSummary({ http, ruleId })} loadAlertTypes={async () => loadAlertTypes({ http })} resolveRule={async (ruleId: Alert['id']) => resolveRule({ http, ruleId })} getHealth={async () => alertingFrameworkHealth({ http })} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 0b35317cad9b3..b086ab147ca1c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -28,8 +28,8 @@ import { AlertAction, AlertAggregations, AlertTaskState, - AlertInstanceSummary, - AlertInstanceStatus, + AlertSummary, + AlertStatus, RawAlertInstance, AlertingFrameworkHealth, AlertNotifyWhenType, @@ -48,8 +48,8 @@ export type { AlertAction, AlertAggregations, AlertTaskState, - AlertInstanceSummary, - AlertInstanceStatus, + AlertSummary, + AlertStatus, RawAlertInstance, AlertingFrameworkHealth, AlertNotifyWhenType, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts similarity index 84% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_instance_summary.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts index 181e6cc940c38..3becd487116f7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts @@ -18,11 +18,11 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { UserAtSpaceScenarios } from '../../scenarios'; // eslint-disable-next-line import/no-default-export -export default function createGetAlertInstanceSummaryTests({ getService }: FtrProviderContext) { +export default function createGetAlertSummaryTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('getAlertInstanceSummary', () => { + describe('getAlertSummary', () => { const objectRemover = new ObjectRemover(supertest); afterEach(() => objectRemover.removeAll()); @@ -30,17 +30,17 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; describe(scenario.id, () => { - it('should handle getAlertInstanceSummary alert request appropriately', async () => { - const { body: createdAlert } = await supertest + it('should handle getAlertSummary request appropriately', async () => { + const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}/_alert_summary` + `${getUrlPrefix(space.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` ) .auth(user.username, user.password); @@ -65,7 +65,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr status_start_date: statusStartDate, status_end_date: statusEndDate, } = response.body; - expect(id).to.equal(createdAlert.id); + expect(id).to.equal(createdRule.id); expect(Date.parse(statusStartDate)).to.be.lessThan(Date.parse(statusEndDate)); const stableBody = omit(response.body, [ @@ -93,8 +93,8 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr } }); - it('should handle getAlertInstanceSummary alert request appropriately when unauthorized', async () => { - const { body: createdAlert } = await supertest + it('should handle getAlertSummary request appropriately when unauthorized', async () => { + const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( @@ -104,11 +104,11 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}/_alert_summary` + `${getUrlPrefix(space.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` ) .auth(user.username, user.password); @@ -150,18 +150,16 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr } }); - it(`shouldn't getAlertInstanceSummary for an alert from another space`, async () => { - const { body: createdAlert } = await supertest + it(`shouldn't getAlertSummary for a rule from another space`, async () => { + const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth - .get( - `${getUrlPrefix('other')}/internal/alerting/rule/${createdAlert.id}/_alert_summary` - ) + .get(`${getUrlPrefix('other')}/internal/alerting/rule/${createdRule.id}/_alert_summary`) .auth(user.username, user.password); expect(response.statusCode).to.eql(404); @@ -176,7 +174,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', - message: `Saved object [alert/${createdAlert.id}] not found`, + message: `Saved object [alert/${createdRule.id}] not found`, }); break; default: @@ -184,7 +182,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr } }); - it(`should handle getAlertInstanceSummary request appropriately when alert doesn't exist`, async () => { + it(`should handle getAlertSummary request appropriately when rule doesn't exist`, async () => { const response = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/1/_alert_summary`) .auth(user.username, user.password); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 114e53f51ec28..b7ef734d40c12 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -40,7 +40,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./execution_status')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./get_alert_state')); - loadTestFile(require.resolve('./get_alert_instance_summary')); + loadTestFile(require.resolve('./get_alert_summary')); loadTestFile(require.resolve('./rule_types')); loadTestFile(require.resolve('./mute_all')); loadTestFile(require.resolve('./mute_instance')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts similarity index 67% rename from x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts index ff55c376c04b2..8cd9a0bbb1290 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_instance_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_summary.ts @@ -19,18 +19,18 @@ import { import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export -export default function createGetAlertInstanceSummaryTests({ getService }: FtrProviderContext) { +export default function createGetAlertSummaryTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const retry = getService('retry'); const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); - describe('getAlertInstanceSummary', () => { + describe('getAlertSummary', () => { const objectRemover = new ObjectRemover(supertest); afterEach(() => objectRemover.removeAll()); - it(`handles non-existant alert`, async () => { + it(`handles non-existent rule`, async () => { await supertest .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/1/_alert_summary`) .expect(404, { @@ -40,17 +40,17 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr }); }); - it('handles no-op alert', async () => { - const { body: createdAlert } = await supertest + it('handles no-op rule', async () => { + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - await waitForEvents(createdAlert.id, ['execute']); + await waitForEvents(createdRule.id, ['execute']); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}/_alert_summary` + `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` ); expect(response.status).to.eql(200); @@ -65,7 +65,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr 'execution_duration', ]); expect(stableBody).to.eql({ - id: createdAlert.id, + id: createdRule.id, name: 'abc', tags: ['foo'], rule_type_id: 'test.noop', @@ -79,16 +79,16 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr }); }); - it('handles no-op alert without waiting for execution event', async () => { - const { body: createdAlert } = await supertest + it('handles no-op rule without waiting for execution event', async () => { + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}/_alert_summary` + `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` ); expect(response.status).to.eql(200); @@ -103,7 +103,7 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr 'execution_duration', ]); expect(stableBody).to.eql({ - id: createdAlert.id, + id: createdRule.id, name: 'abc', tags: ['foo'], rule_type_id: 'test.noop', @@ -119,17 +119,17 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr it('handles dateStart parameter', async () => { const dateStart = '2020-08-08T08:08:08.008Z'; - const { body: createdAlert } = await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - await waitForEvents(createdAlert.id, ['execute']); + await waitForEvents(createdRule.id, ['execute']); const response = await supertest.get( `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${ - createdAlert.id + createdRule.id }/_alert_summary?date_start=${dateStart}` ); expect(response.status).to.eql(200); @@ -139,18 +139,18 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr }); it('handles invalid dateStart parameter', async () => { - const { body: createdAlert } = await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - await waitForEvents(createdAlert.id, ['execute']); + await waitForEvents(createdRule.id, ['execute']); const dateStart = 'X0X0-08-08T08:08:08.008Z'; const response = await supertest.get( `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${ - createdAlert.id + createdRule.id }/_alert_summary?date_start=${dateStart}` ); expect(response.status).to.eql(400); @@ -161,18 +161,18 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr }); }); - it('handles muted instances', async () => { - const { body: createdAlert } = await supertest + it('handles muted alerts', async () => { + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - await alertUtils.muteInstance(createdAlert.id, '1'); - await waitForEvents(createdAlert.id, ['execute']); + await alertUtils.muteInstance(createdRule.id, '1'); + await waitForEvents(createdRule.id, ['execute']); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}/_alert_summary` + `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` ); expect(response.status).to.eql(200); @@ -184,18 +184,18 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr }); }); - it('handles alert errors', async () => { + it('handles rule errors', async () => { const dateNow = Date.now(); - const { body: createdAlert } = await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ rule_type_id: 'test.throw' })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - await waitForEvents(createdAlert.id, ['execute']); + await waitForEvents(createdRule.id, ['execute']); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}/_alert_summary` + `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` ); const { error_messages: errorMessages } = response.body; expect(errorMessages.length).to.be.greaterThan(0); @@ -204,15 +204,15 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr expect(errorMessage.message).to.be('this alert is intended to fail'); }); - it('handles multi-instance status', async () => { - // pattern of when the alert should fire + it('handles multi-alert status', async () => { + // pattern of when the rule should fire const pattern = { - instanceA: [true, true, true, true], - instanceB: [true, true, false, false], - instanceC: [true, true, true, true], + alertA: [true, true, true, true], + alertB: [true, true, false, false], + alertC: [true, true, true, true], }; - const { body: createdAlert } = await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( @@ -223,51 +223,51 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr }) ) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - await alertUtils.muteInstance(createdAlert.id, 'instanceC'); - await alertUtils.muteInstance(createdAlert.id, 'instanceD'); - await waitForEvents(createdAlert.id, ['new-instance', 'recovered-instance']); + await alertUtils.muteInstance(createdRule.id, 'alertC'); + await alertUtils.muteInstance(createdRule.id, 'alertD'); + await waitForEvents(createdRule.id, ['new-instance', 'recovered-instance']); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}/_alert_summary` + `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` ); - const actualInstances = response.body.alerts; - const expectedInstances = { - instanceA: { + const actualAlerts = response.body.alerts; + const expectedAlerts = { + alertA: { status: 'Active', muted: false, actionGroupId: 'default', - activeStartDate: actualInstances.instanceA.activeStartDate, + activeStartDate: actualAlerts.alertA.activeStartDate, }, - instanceB: { + alertB: { status: 'OK', muted: false, }, - instanceC: { + alertC: { status: 'Active', muted: true, actionGroupId: 'default', - activeStartDate: actualInstances.instanceC.activeStartDate, + activeStartDate: actualAlerts.alertC.activeStartDate, }, - instanceD: { + alertD: { status: 'OK', muted: true, }, }; - expect(actualInstances).to.eql(expectedInstances); + expect(actualAlerts).to.eql(expectedAlerts); }); describe('legacy', () => { - it('handles multi-instance status', async () => { + it('handles multi-alert status', async () => { // pattern of when the alert should fire const pattern = { - instanceA: [true, true, true, true], - instanceB: [true, true, false, false], - instanceC: [true, true, true, true], + alertA: [true, true, true, true], + alertB: [true, true, false, false], + alertC: [true, true, true, true], }; - const { body: createdAlert } = await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( @@ -278,39 +278,39 @@ export default function createGetAlertInstanceSummaryTests({ getService }: FtrPr }) ) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); - await alertUtils.muteInstance(createdAlert.id, 'instanceC'); - await alertUtils.muteInstance(createdAlert.id, 'instanceD'); - await waitForEvents(createdAlert.id, ['new-instance', 'recovered-instance']); + await alertUtils.muteInstance(createdRule.id, 'alertC'); + await alertUtils.muteInstance(createdRule.id, 'alertD'); + await waitForEvents(createdRule.id, ['new-instance', 'recovered-instance']); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/_instance_summary` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdRule.id}/_instance_summary` ); - const actualInstances = response.body.instances; - const expectedInstances = { - instanceA: { + const actualAlerts = response.body.instances; + const expectedAlerts = { + alertA: { status: 'Active', muted: false, actionGroupId: 'default', - activeStartDate: actualInstances.instanceA.activeStartDate, + activeStartDate: actualAlerts.alertA.activeStartDate, }, - instanceB: { + alertB: { status: 'OK', muted: false, }, - instanceC: { + alertC: { status: 'Active', muted: true, actionGroupId: 'default', - activeStartDate: actualInstances.instanceC.activeStartDate, + activeStartDate: actualAlerts.alertC.activeStartDate, }, - instanceD: { + alertD: { status: 'OK', muted: true, }, }; - expect(actualInstances).to.eql(expectedInstances); + expect(actualAlerts).to.eql(expectedAlerts); }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 531046013263f..f9cb6175e6fa6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -22,7 +22,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./get_alert_state')); - loadTestFile(require.resolve('./get_alert_instance_summary')); + loadTestFile(require.resolve('./get_alert_summary')); loadTestFile(require.resolve('./rule_types')); loadTestFile(require.resolve('./event_log')); loadTestFile(require.resolve('./execution_status')); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 0b8bb38010fc1..4629fd9d5e56e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -16,7 +16,7 @@ import { getTestAlertData, getTestActionData } from '../../lib/get_test_data'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); - const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'ruleDetailsUI']); const browser = getService('browser'); const log = getService('log'); const retry = getService('retry'); @@ -25,33 +25,33 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const comboBox = getService('comboBox'); const objectRemover = new ObjectRemover(supertest); - async function createActionManualCleanup(overwrites: Record = {}) { - const { body: createdAction } = await supertest + async function createConnectorManualCleanup(overwrites: Record = {}) { + const { body: createdConnector } = await supertest .post(`/api/actions/connector`) .set('kbn-xsrf', 'foo') .send(getTestActionData(overwrites)) .expect(200); - return createdAction; + return createdConnector; } - async function createAction(overwrites: Record = {}) { - const createdAction = await createActionManualCleanup(overwrites); - objectRemover.add(createdAction.id, 'action', 'actions'); - return createdAction; + async function createConnector(overwrites: Record = {}) { + const createdConnector = await createConnectorManualCleanup(overwrites); + objectRemover.add(createdConnector.id, 'action', 'actions'); + return createdConnector; } - async function createAlert(overwrites: Record = {}) { - const { body: createdAlert } = await supertest + async function createRule(overwrites: Record = {}) { + const { body: createdRule } = await supertest .post(`/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestAlertData(overwrites)) .expect(200); - objectRemover.add(createdAlert.id, 'alert', 'alerts'); - return createdAlert; + objectRemover.add(createdRule.id, 'alert', 'alerts'); + return createdRule; } - async function createAlwaysFiringAlert(overwrites: Record = {}) { - const { body: createdAlert } = await supertest + async function createAlwaysFiringRule(overwrites: Record = {}) { + const { body: createdRule } = await supertest .post(`/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send( @@ -61,26 +61,26 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }) ) .expect(200); - objectRemover.add(createdAlert.id, 'alert', 'alerts'); - return createdAlert; + objectRemover.add(createdRule.id, 'alert', 'alerts'); + return createdRule; } - async function createActions(testRunUuid: string) { + async function createConnectors(testRunUuid: string) { return await Promise.all([ - createAction({ name: `slack-${testRunUuid}-${0}` }), - createAction({ name: `slack-${testRunUuid}-${1}` }), + createConnector({ name: `slack-${testRunUuid}-${0}` }), + createConnector({ name: `slack-${testRunUuid}-${1}` }), ]); } - async function createAlertWithActionsAndParams( + async function createRuleWithActionsAndParams( testRunUuid: string, params: Record = {} ) { - const actions = await createActions(testRunUuid); - return await createAlwaysFiringAlert({ - name: `test-alert-${testRunUuid}`, - actions: actions.map((action) => ({ - id: action.id, + const connectors = await createConnectors(testRunUuid); + return await createAlwaysFiringRule({ + name: `test-rule-${testRunUuid}`, + actions: connectors.map((connector) => ({ + id: connector.id, group: 'default', params: { message: 'from alert 1s', @@ -91,18 +91,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - async function getAlertInstanceSummary(alertId: string) { + async function getAlertSummary(ruleId: string) { const { body: summary } = await supertest - .get(`/internal/alerting/rule/${encodeURIComponent(alertId)}/_alert_summary`) + .get(`/internal/alerting/rule/${encodeURIComponent(ruleId)}/_alert_summary`) .expect(200); return summary; } - async function muteAlertInstance(alertId: string, alertInstanceId: string) { + async function muteAlert(ruleId: string, alertId: string) { const { body: response } = await supertest .post( - `/api/alerting/rule/${encodeURIComponent(alertId)}/alert/${encodeURIComponent( - alertInstanceId + `/api/alerting/rule/${encodeURIComponent(ruleId)}/alert/${encodeURIComponent( + alertId )}/_mute` ) .set('kbn-xsrf', 'foo') @@ -111,14 +111,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return response; } - describe('Alert Details', function () { + describe('Rule Details', function () { describe('Header', function () { const testRunUuid = uuid.v4(); before(async () => { await pageObjects.common.navigateToApp('triggersActions'); - const alert = await createAlertWithActionsAndParams(testRunUuid); + const rule = await createRuleWithActionsAndParams(testRunUuid); - // refresh to see alert + // refresh to see rule await browser.refresh(); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -127,25 +127,25 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.existOrFail('alertsList'); // click on first alert - await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); }); after(async () => { await objectRemover.removeAll(); }); - it('renders the alert details', async () => { - const headingText = await pageObjects.alertDetailsUI.getHeadingText(); - expect(headingText.includes(`test-alert-${testRunUuid}`)).to.be(true); + it('renders the rule details', async () => { + const headingText = await pageObjects.ruleDetailsUI.getHeadingText(); + expect(headingText.includes(`test-rule-${testRunUuid}`)).to.be(true); - const alertType = await pageObjects.alertDetailsUI.getAlertType(); - expect(alertType).to.be(`Always Firing`); + const ruleType = await pageObjects.ruleDetailsUI.getRuleType(); + expect(ruleType).to.be(`Always Firing`); - const { actionType } = await pageObjects.alertDetailsUI.getActionsLabels(); - expect(actionType).to.be(`Slack`); + const { connectorType } = await pageObjects.ruleDetailsUI.getActionsLabels(); + expect(connectorType).to.be(`Slack`); }); - it('should disable the alert', async () => { + it('should disable the rule', async () => { const enableSwitch = await testSubjects.find('enableSwitch'); const isChecked = await enableSwitch.getAttribute('aria-checked'); @@ -160,7 +160,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(isCheckedAfterDisabling).to.eql('false'); }); - it('shouldnt allow you to mute a disabled alert', async () => { + it('shouldnt allow you to mute a disabled rule', async () => { const disabledEnableSwitch = await testSubjects.find('enableSwitch'); expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false'); @@ -176,7 +176,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(isDisabledMuteAfterDisabling).to.eql('false'); }); - it('should reenable a disabled the alert', async () => { + it('should reenable a disabled the rule', async () => { const enableSwitch = await testSubjects.find('enableSwitch'); const isChecked = await enableSwitch.getAttribute('aria-checked'); @@ -191,7 +191,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(isCheckedAfterDisabling).to.eql('true'); }); - it('should mute the alert', async () => { + it('should mute the rule', async () => { const muteSwitch = await testSubjects.find('muteSwitch'); const isChecked = await muteSwitch.getAttribute('aria-checked'); @@ -204,7 +204,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(isCheckedAfterDisabling).to.eql('true'); }); - it('should unmute the alert', async () => { + it('should unmute the rule', async () => { const muteSwitch = await testSubjects.find('muteSwitch'); const isChecked = await muteSwitch.getAttribute('aria-checked'); @@ -218,13 +218,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('Edit alert button', function () { - const alertName = uuid.v4(); - const updatedAlertName = `Changed Alert Name ${alertName}`; + describe('Edit rule button', function () { + const ruleName = uuid.v4(); + const updatedRuleName = `Changed Rule Name ${ruleName}`; before(async () => { - await createAlwaysFiringAlert({ - name: alertName, + await createAlwaysFiringRule({ + name: ruleName, rule_type_id: '.index-threshold', params: { aggType: 'count', @@ -251,10 +251,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await objectRemover.removeAll(); }); - it('should open edit alert flyout', async () => { + it('should open edit rule flyout', async () => { await pageObjects.common.navigateToApp('triggersActions'); - // refresh to see alert + // refresh to see rule await browser.refresh(); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -262,30 +262,30 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertsList'); - // click on first alert - await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alertName); + // click on first rule + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(ruleName); const editButton = await testSubjects.find('openEditAlertFlyoutButton'); await editButton.click(); expect(await testSubjects.exists('hasActionsDisabled')).to.eql(false); - await testSubjects.setValue('alertNameInput', updatedAlertName, { + await testSubjects.setValue('alertNameInput', updatedRuleName, { clearWithKeyboard: true, }); await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); const toastTitle = await pageObjects.common.closeToast(); - expect(toastTitle).to.eql(`Updated '${updatedAlertName}'`); + expect(toastTitle).to.eql(`Updated '${updatedRuleName}'`); - const headingText = await pageObjects.alertDetailsUI.getHeadingText(); - expect(headingText.includes(updatedAlertName)).to.be(true); + const headingText = await pageObjects.ruleDetailsUI.getHeadingText(); + expect(headingText.includes(updatedRuleName)).to.be(true); }); - it('should reset alert when canceling an edit', async () => { + it('should reset rule when canceling an edit', async () => { await pageObjects.common.navigateToApp('triggersActions'); - // refresh to see alert + // refresh to see rule await browser.refresh(); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -293,8 +293,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertsList'); - // click on first alert - await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(updatedAlertName); + // click on first rule + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(updatedRuleName); const editButton = await testSubjects.find('openEditAlertFlyoutButton'); await editButton.click(); @@ -312,11 +312,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const nameInputAfterCancel = await testSubjects.find('alertNameInput'); const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); - expect(textAfterCancel).to.eql(updatedAlertName); + expect(textAfterCancel).to.eql(updatedRuleName); }); }); - describe('Edit alert with deleted connector', function () { + describe('Edit rule with deleted connector', function () { const testRunUuid = uuid.v4(); afterEach(async () => { @@ -324,23 +324,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should show and update deleted connectors when there are existing connectors of the same type', async () => { - const action = await createActionManualCleanup({ + const connector = await createConnectorManualCleanup({ name: `slack-${testRunUuid}-${0}`, }); await pageObjects.common.navigateToApp('triggersActions'); - const alert = await createAlwaysFiringAlert({ + const rule = await createAlwaysFiringRule({ name: testRunUuid, actions: [ { group: 'default', - id: action.id, + id: connector.id, params: { level: 'info', message: ' {{context.message}}' }, }, ], }); - // refresh to see alert + // refresh to see rule await browser.refresh(); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -349,7 +349,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // delete connector await pageObjects.triggersActionsUI.changeTabs('connectorsTab'); - await pageObjects.triggersActionsUI.searchConnectors(action.name); + await pageObjects.triggersActionsUI.searchConnectors(connector.name); await testSubjects.click('deleteConnector'); await testSubjects.existOrFail('deleteIdsConfirmation'); await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton'); @@ -360,7 +360,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // click on first alert await pageObjects.triggersActionsUI.changeTabs('rulesTab'); - await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); const editButton = await testSubjects.find('openEditAlertFlyoutButton'); await editButton.click(); @@ -374,7 +374,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should show and update deleted connectors when there are no existing connectors of the same type', async () => { - const action = await createActionManualCleanup({ + const connector = await createConnectorManualCleanup({ name: `index-${testRunUuid}-${0}`, connector_type_id: '.index', config: { @@ -384,17 +384,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); await pageObjects.common.navigateToApp('triggersActions'); - const alert = await createAlwaysFiringAlert({ + const alert = await createAlwaysFiringRule({ name: testRunUuid, actions: [ { group: 'default', - id: action.id, + id: connector.id, params: { level: 'info', message: ' {{context.message}}' }, }, { group: 'other', - id: action.id, + id: connector.id, params: { level: 'info', message: ' {{context.message}}' }, }, ], @@ -409,7 +409,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // delete connector await pageObjects.triggersActionsUI.changeTabs('connectorsTab'); - await pageObjects.triggersActionsUI.searchConnectors(action.name); + await pageObjects.triggersActionsUI.searchConnectors(connector.name); await testSubjects.click('deleteConnector'); await testSubjects.existOrFail('deleteIdsConfirmation'); await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton'); @@ -418,7 +418,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql('Deleted 1 connector'); - // click on first alert + // click on first rule await pageObjects.triggersActionsUI.changeTabs('rulesTab'); await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); @@ -454,7 +454,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); describe('View In App', function () { - const alertName = uuid.v4(); + const ruleName = uuid.v4(); beforeEach(async () => { await pageObjects.common.navigateToApp('triggersActions'); @@ -464,74 +464,74 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await objectRemover.removeAll(); }); - it('renders the alert details view in app button', async () => { - const alert = await createAlert({ - name: alertName, + it('renders the rule details view in app button', async () => { + const rule = await createRule({ + name: ruleName, consumer: 'alerting_fixture', }); - // refresh to see alert + // refresh to see rule await browser.refresh(); await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content await testSubjects.existOrFail('alertsList'); - // click on first alert - await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + // click on first rule + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); - expect(await pageObjects.alertDetailsUI.isViewInAppEnabled()).to.be(true); + expect(await pageObjects.ruleDetailsUI.isViewInAppEnabled()).to.be(true); - await pageObjects.alertDetailsUI.clickViewInApp(); + await pageObjects.ruleDetailsUI.clickViewInApp(); - expect(await pageObjects.alertDetailsUI.getNoOpAppTitle()).to.be(`View Rule ${alert.id}`); + expect(await pageObjects.ruleDetailsUI.getNoOpAppTitle()).to.be(`View Rule ${rule.id}`); }); - it('renders a disabled alert details view in app button', async () => { - const alert = await createAlwaysFiringAlert({ - name: `test-alert-disabled-nav`, + it('renders a disabled rule details view in app button', async () => { + const rule = await createAlwaysFiringRule({ + name: `test-rule-disabled-nav`, }); - // refresh to see alert + // refresh to see rule await browser.refresh(); await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content await testSubjects.existOrFail('alertsList'); - // click on first alert - await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + // click on first rule + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); - expect(await pageObjects.alertDetailsUI.isViewInAppDisabled()).to.be(true); + expect(await pageObjects.ruleDetailsUI.isViewInAppDisabled()).to.be(true); }); }); - describe('Alert Instances', function () { + describe('Alerts', function () { const testRunUuid = uuid.v4(); - let alert: any; + let rule: any; before(async () => { await pageObjects.common.navigateToApp('triggersActions'); - const instances = [{ id: 'us-central' }, { id: 'us-east' }, { id: 'us-west' }]; - alert = await createAlertWithActionsAndParams(testRunUuid, { - instances, + const alerts = [{ id: 'us-central' }, { id: 'us-east' }, { id: 'us-west' }]; + rule = await createRuleWithActionsAndParams(testRunUuid, { + instances: alerts, }); - // refresh to see alert + // refresh to see rule await browser.refresh(); await pageObjects.header.waitUntilLoadingHasFinished(); // Verify content await testSubjects.existOrFail('alertsList'); - // click on first alert - await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + // click on first rule + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); // await first run to complete so we have an initial state await retry.try(async () => { - const { alerts: alertInstances } = await getAlertInstanceSummary(alert.id); - expect(Object.keys(alertInstances).length).to.eql(instances.length); + const { alerts: alertInstances } = await getAlertSummary(rule.id); + expect(Object.keys(alertInstances).length).to.eql(alerts.length); }); }); @@ -539,7 +539,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await objectRemover.removeAll(); }); - it('renders the active alert instances', async () => { + it('renders the active alerts', async () => { // refresh to ensure Api call and UI are looking at freshest output await browser.refresh(); @@ -547,66 +547,61 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const { actionGroups } = alwaysFiringAlertType; // Verify content - await testSubjects.existOrFail('alertInstancesList'); + await testSubjects.existOrFail('alertsList'); const actionGroupNameFromId = (actionGroupId: string) => actionGroups.find( (actionGroup: { id: string; name: string }) => actionGroup.id === actionGroupId )?.name; - const summary = await getAlertInstanceSummary(alert.id); - const dateOnAllInstancesFromApiResponse: Record = mapValues( + const summary = await getAlertSummary(rule.id); + const dateOnAllAlertsFromApiResponse: Record = mapValues( summary.alerts, - (instance) => instance.activeStartDate + (a) => a.activeStartDate ); - const actionGroupNameOnAllInstancesFromApiResponse = mapValues( - summary.alerts, - (instance) => { - const name = actionGroupNameFromId(instance.actionGroupId); - return name ? ` (${name})` : ''; - } - ); + const actionGroupNameOnAllInstancesFromApiResponse = mapValues(summary.alerts, (a) => { + const name = actionGroupNameFromId(a.actionGroupId); + return name ? ` (${name})` : ''; + }); log.debug( - `API RESULT: ${Object.entries(dateOnAllInstancesFromApiResponse) + `API RESULT: ${Object.entries(dateOnAllAlertsFromApiResponse) .map(([id, date]) => `${id}: ${moment(date).utc()}`) .join(', ')}` ); - const instancesList: any[] = await pageObjects.alertDetailsUI.getAlertInstancesList(); - expect(instancesList.map((instance) => omit(instance, 'duration'))).to.eql([ + const alertsList: any[] = await pageObjects.ruleDetailsUI.getAlertsList(); + expect(alertsList.map((a) => omit(a, 'duration'))).to.eql([ { - instance: 'us-central', + alert: 'us-central', status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-central']}`, - start: moment(dateOnAllInstancesFromApiResponse['us-central']) + start: moment(dateOnAllAlertsFromApiResponse['us-central']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { - instance: 'us-east', + alert: 'us-east', status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-east']}`, - start: moment(dateOnAllInstancesFromApiResponse['us-east']) + start: moment(dateOnAllAlertsFromApiResponse['us-east']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, { - instance: 'us-west', + alert: 'us-west', status: `Active${actionGroupNameOnAllInstancesFromApiResponse['us-west']}`, - start: moment(dateOnAllInstancesFromApiResponse['us-west']) + start: moment(dateOnAllAlertsFromApiResponse['us-west']) .utc() .format('D MMM YYYY @ HH:mm:ss'), }, ]); - const durationEpoch = moment( - await pageObjects.alertDetailsUI.getAlertInstanceDurationEpoch() - ).utc(); + const durationEpoch = moment(await pageObjects.ruleDetailsUI.getAlertDurationEpoch()).utc(); log.debug(`DURATION EPOCH is: ${durationEpoch}]`); const durationFromInstanceInApiUntilPageLoad = mapValues( - dateOnAllInstancesFromApiResponse, + dateOnAllAlertsFromApiResponse, // time from Alert Instance until pageload (AKA durationEpoch) (date) => { const durationFromApiResuiltToEpoch = moment.duration( @@ -621,11 +616,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } ); - instancesList - .map((alertInstance) => ({ - id: alertInstance.instance, + alertsList + .map((a) => ({ + id: a.alert, // time from Alert Instance used to render the list until pageload (AKA durationEpoch) - duration: moment.duration(alertInstance.duration), + duration: moment.duration(a.duration), })) .forEach(({ id, duration: durationAsItAppearsOnList }) => { log.debug( @@ -642,19 +637,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - it('renders the muted inactive alert instances', async () => { - // mute an alert instance that doesn't exist - await muteAlertInstance(alert.id, 'eu/east'); + it('renders the muted inactive alerts', async () => { + // mute an alert that doesn't exist + await muteAlert(rule.id, 'eu/east'); - // refresh to see alert + // refresh to see rule await browser.refresh(); - const instancesList: any[] = await pageObjects.alertDetailsUI.getAlertInstancesList(); - expect( - instancesList.filter((alertInstance) => alertInstance.instance === 'eu/east') - ).to.eql([ + const alertsList: any[] = await pageObjects.ruleDetailsUI.getAlertsList(); + expect(alertsList.filter((a) => a.alert === 'eu/east')).to.eql([ { - instance: 'eu/east', + alert: 'eu/east', status: 'Recovered', start: '', duration: '', @@ -662,77 +655,77 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); }); - it('allows the user to mute a specific instance', async () => { + it('allows the user to mute a specific alert', async () => { // Verify content - await testSubjects.existOrFail('alertInstancesList'); + await testSubjects.existOrFail('alertsList'); log.debug(`Ensuring us-central is not muted`); - await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-central', false); + await pageObjects.ruleDetailsUI.ensureAlertMuteState('us-central', false); log.debug(`Muting us-central`); - await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('us-central'); + await pageObjects.ruleDetailsUI.clickAlertMuteButton('us-central'); log.debug(`Ensuring us-central is muted`); - await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-central', true); + await pageObjects.ruleDetailsUI.ensureAlertMuteState('us-central', true); }); - it('allows the user to unmute a specific instance', async () => { + it('allows the user to unmute a specific alert', async () => { // Verify content - await testSubjects.existOrFail('alertInstancesList'); + await testSubjects.existOrFail('alertsList'); log.debug(`Ensuring us-east is not muted`); - await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-east', false); + await pageObjects.ruleDetailsUI.ensureAlertMuteState('us-east', false); log.debug(`Muting us-east`); - await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('us-east'); + await pageObjects.ruleDetailsUI.clickAlertMuteButton('us-east'); log.debug(`Ensuring us-east is muted`); - await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-east', true); + await pageObjects.ruleDetailsUI.ensureAlertMuteState('us-east', true); log.debug(`Unmuting us-east`); - await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('us-east'); + await pageObjects.ruleDetailsUI.clickAlertMuteButton('us-east'); log.debug(`Ensuring us-east is not muted`); - await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-east', false); + await pageObjects.ruleDetailsUI.ensureAlertMuteState('us-east', false); }); - it('allows the user unmute an inactive instance', async () => { + it('allows the user unmute an inactive alert', async () => { log.debug(`Ensuring eu/east is muted`); - await pageObjects.alertDetailsUI.ensureAlertInstanceMute('eu/east', true); + await pageObjects.ruleDetailsUI.ensureAlertMuteState('eu/east', true); log.debug(`Unmuting eu/east`); - await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('eu/east'); + await pageObjects.ruleDetailsUI.clickAlertMuteButton('eu/east'); log.debug(`Ensuring eu/east is removed from list`); - await pageObjects.alertDetailsUI.ensureAlertInstanceExistance('eu/east', false); + await pageObjects.ruleDetailsUI.ensureAlertExistence('eu/east', false); }); }); - describe('Alert Instance Pagination', function () { + describe('Alert Pagination', function () { const testRunUuid = uuid.v4(); - let alert: any; + let rule: any; before(async () => { await pageObjects.common.navigateToApp('triggersActions'); - const instances = flatten( + const alerts = flatten( range(10).map((index) => [ { id: `us-central-${index}` }, { id: `us-east-${index}` }, { id: `us-west-${index}` }, ]) ); - alert = await createAlertWithActionsAndParams(testRunUuid, { - instances, + rule = await createRuleWithActionsAndParams(testRunUuid, { + instances: alerts, }); // await first run to complete so we have an initial state await retry.try(async () => { - const { alerts: alertInstances } = await getAlertInstanceSummary(alert.id); - expect(Object.keys(alertInstances).length).to.eql(instances.length); + const { alerts: alertInstances } = await getAlertSummary(rule.id); + expect(Object.keys(alertInstances).length).to.eql(alerts.length); }); - // refresh to see alert + // refresh to see rule await browser.refresh(); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -740,8 +733,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertsList'); - // click on first alert - await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + // click on first rule + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name); }); after(async () => { @@ -751,28 +744,28 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const PAGE_SIZE = 10; it('renders the first page', async () => { // Verify content - await testSubjects.existOrFail('alertInstancesList'); + await testSubjects.existOrFail('alertsList'); - const { alerts: alertInstances } = await getAlertInstanceSummary(alert.id); + const { alerts: alertInstances } = await getAlertSummary(rule.id); - const items = await pageObjects.alertDetailsUI.getAlertInstancesList(); + const items = await pageObjects.ruleDetailsUI.getAlertsList(); expect(items.length).to.eql(PAGE_SIZE); const [firstItem] = items; - expect(firstItem.instance).to.eql(Object.keys(alertInstances)[0]); + expect(firstItem.alert).to.eql(Object.keys(alertInstances)[0]); }); it('navigates to the next page', async () => { // Verify content - await testSubjects.existOrFail('alertInstancesList'); + await testSubjects.existOrFail('alertsList'); - const { alerts: alertInstances } = await getAlertInstanceSummary(alert.id); + const { alerts: alertInstances } = await getAlertSummary(rule.id); - await pageObjects.alertDetailsUI.clickPaginationNextPage(); + await pageObjects.ruleDetailsUI.clickPaginationNextPage(); await retry.try(async () => { - const [firstItem] = await pageObjects.alertDetailsUI.getAlertInstancesList(); - expect(firstItem.instance).to.eql(Object.keys(alertInstances)[PAGE_SIZE]); + const [firstItem] = await pageObjects.ruleDetailsUI.getAlertsList(); + expect(firstItem.alert).to.eql(Object.keys(alertInstances)[PAGE_SIZE]); }); }); }); diff --git a/x-pack/test/functional_with_es_ssl/page_objects/index.ts b/x-pack/test/functional_with_es_ssl/page_objects/index.ts index 4fa08b05604b6..78a55a2d4f458 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/index.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/index.ts @@ -7,10 +7,10 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { TriggersActionsPageProvider } from './triggers_actions_ui_page'; -import { AlertDetailsPageProvider } from './alert_details'; +import { RuleDetailsPageProvider } from './rule_details'; export const pageObjects = { ...xpackFunctionalPageObjects, triggersActionsUI: TriggersActionsPageProvider, - alertDetailsUI: AlertDetailsPageProvider, + ruleDetailsUI: RuleDetailsPageProvider, }; diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts similarity index 60% rename from x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts rename to x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts index 8740deaeddd6e..9e958d4207e56 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; -export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { +export function RuleDetailsPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const log = getService('log'); @@ -18,80 +18,76 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { async getHeadingText() { return await testSubjects.getVisibleText('alertDetailsTitle'); }, - async getAlertType() { + async getRuleType() { return await testSubjects.getVisibleText('alertTypeLabel'); }, async getActionsLabels() { return { - actionType: await testSubjects.getVisibleText('actionTypeLabel'), + connectorType: await testSubjects.getVisibleText('actionTypeLabel'), }; }, - async getAlertInstancesList() { + async getAlertsList() { const table = await find.byCssSelector( - '.euiBasicTable[data-test-subj="alertInstancesList"]:not(.euiBasicTable-loading)' + '.euiBasicTable[data-test-subj="alertsList"]:not(.euiBasicTable-loading)' ); const $ = await table.parseDomContent(); - return $.findTestSubjects('alert-instance-row') + return $.findTestSubjects('alert-row') .toArray() .map((row) => { return { - instance: $(row) - .findTestSubject('alertInstancesTableCell-instance') + alert: $(row) + .findTestSubject('alertsTableCell-alert') .find('.euiTableCellContent') .text(), status: $(row) - .findTestSubject('alertInstancesTableCell-status') + .findTestSubject('alertsTableCell-status') .find('.euiTableCellContent') .text(), start: $(row) - .findTestSubject('alertInstancesTableCell-start') + .findTestSubject('alertsTableCell-start') .find('.euiTableCellContent') .text(), duration: $(row) - .findTestSubject('alertInstancesTableCell-duration') + .findTestSubject('alertsTableCell-duration') .find('.euiTableCellContent') .text(), }; }); }, - async getAlertInstanceDurationEpoch(): Promise { - const alertInstancesDurationEpoch = await find.byCssSelector( - 'input[data-test-subj="alertInstancesDurationEpoch"]' + async getAlertDurationEpoch(): Promise { + const alertDurationEpoch = await find.byCssSelector( + 'input[data-test-subj="alertsDurationEpoch"]' ); - return parseInt(await alertInstancesDurationEpoch.getAttribute('value'), 10); + return parseInt(await alertDurationEpoch.getAttribute('value'), 10); }, - async clickAlertInstanceMuteButton(instance: string) { - const muteAlertInstanceButton = await testSubjects.find( - `muteAlertInstanceButton_${instance}` - ); - await muteAlertInstanceButton.click(); + async clickAlertMuteButton(alert: string) { + const muteAlertButton = await testSubjects.find(`muteAlertButton_${alert}`); + await muteAlertButton.click(); }, - async ensureAlertInstanceMute(instance: string, isMuted: boolean) { + async ensureAlertMuteState(alert: string, isMuted: boolean) { await retry.try(async () => { - const muteAlertInstanceButton = await testSubjects.find( - `muteAlertInstanceButton_${instance}` - ); - log.debug(`checked:${await muteAlertInstanceButton.getAttribute('aria-checked')}`); - expect(await muteAlertInstanceButton.getAttribute('aria-checked')).to.eql( + const muteAlertButton = await testSubjects.find(`muteAlertButton_${alert}`); + log.debug(`checked:${await muteAlertButton.getAttribute('aria-checked')}`); + expect(await muteAlertButton.getAttribute('aria-checked')).to.eql( isMuted ? 'true' : 'false' ); }); }, - async ensureAlertInstanceExistance(instance: string, shouldExist: boolean) { + async ensureAlertExistence(alert: string, shouldExist: boolean) { await retry.try(async () => { const table = await find.byCssSelector( - '.euiBasicTable[data-test-subj="alertInstancesList"]:not(.euiBasicTable-loading)' + '.euiBasicTable[data-test-subj="alertsList"]:not(.euiBasicTable-loading)' ); const $ = await table.parseDomContent(); expect( - $.findTestSubjects('alert-instance-row') + $.findTestSubjects('alert-row') .toArray() .filter( (row) => $(row) - .findTestSubject('alertInstancesTableCell-instance') + .findTestSubject('alertsTableCell-alert') .find('.euiTableCellContent') - .text() === instance + .text() === alert ) ).to.eql(shouldExist ? 1 : 0); }); From 7d90bad9604aab74a6477f633b6de1f2d7094f8f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 8 Nov 2021 18:39:14 -0700 Subject: [PATCH 17/98] [Maps] convert TileLayer and VectorTileLayer to TS (#117745) * [Maps] convert TileLayer and VectorTileLayer to TS * commit using @elastic.co Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../maps/public/classes/layers/layer.tsx | 2 +- .../classes/layers/tile_layer/tile_layer.d.ts | 21 --- .../{tile_layer.js => tile_layer.ts} | 48 ++++-- .../vector_tile_layer/vector_tile_layer.d.ts | 13 -- ...or_tile_layer.js => vector_tile_layer.tsx} | 160 ++++++++++++------ 5 files changed, 142 insertions(+), 102 deletions(-) delete mode 100644 x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts rename x-pack/plugins/maps/public/classes/layers/tile_layer/{tile_layer.js => tile_layer.ts} (66%) delete mode 100644 x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.d.ts rename x-pack/plugins/maps/public/classes/layers/vector_tile_layer/{vector_tile_layer.js => vector_tile_layer.tsx} (63%) diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 051115a072608..21fee2e3dfdce 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -466,7 +466,7 @@ export class AbstractLayer implements ILayer { return null; } - isBasemap(): boolean { + isBasemap(order: number): boolean { return false; } } diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts deleted file mode 100644 index e83eff53c57c8..0000000000000 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AbstractLayer } from '../layer'; -import { ITMSSource } from '../../sources/tms_source'; -import { LayerDescriptor } from '../../../../common/descriptor_types'; - -interface ITileLayerArguments { - source: ITMSSource; - layerDescriptor: LayerDescriptor; -} - -export class TileLayer extends AbstractLayer { - static type: string; - - constructor(args: ITileLayerArguments); -} 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.ts similarity index 66% rename from x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js rename to x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.ts index 6ff5377a06ed9..b8a9924606198 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.ts @@ -5,25 +5,41 @@ * 2.0. */ -import { AbstractLayer } from '../layer'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import _ from 'lodash'; +import { AbstractLayer } from '../layer'; import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { LayerDescriptor } from '../../../../common/descriptor_types'; import { TileStyle } from '../../styles/tile/tile_style'; +import { ITMSSource } from '../../sources/tms_source'; +import { DataRequestContext } from '../../../actions'; + +export interface ITileLayerArguments { + source: ITMSSource; + layerDescriptor: LayerDescriptor; +} +// TODO - rename to RasterTileLayer export class TileLayer extends AbstractLayer { - static createDescriptor(options, mapColors) { - const tileLayerDescriptor = super.createDescriptor(options, mapColors); + static createDescriptor(options: Partial) { + const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = LAYER_TYPE.TILE; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; } - constructor({ source, layerDescriptor }) { + private readonly _style: TileStyle; + + constructor({ source, layerDescriptor }: ITileLayerArguments) { super({ source, layerDescriptor }); this._style = new TileStyle(); } + getSource(): ITMSSource { + return super.getSource() as ITMSSource; + } + getStyleForEditing() { return this._style; } @@ -36,10 +52,10 @@ export class TileLayer extends AbstractLayer { return this._style; } - async syncData({ startLoading, stopLoading, onLoadError, dataFilters }) { + async syncData({ startLoading, stopLoading, onLoadError, dataFilters }: DataRequestContext) { const sourceDataRequest = this.getSourceDataRequest(); if (sourceDataRequest) { - //data is immmutable + // data is immmutable return; } const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); @@ -60,28 +76,28 @@ export class TileLayer extends AbstractLayer { return [this._getMbLayerId()]; } - ownsMbLayerId(mbLayerId) { + ownsMbLayerId(mbLayerId: string) { return this._getMbLayerId() === mbLayerId; } - ownsMbSourceId(mbSourceId) { + ownsMbSourceId(mbSourceId: string) { return this.getId() === mbSourceId; } - syncLayerWithMB(mbMap) { + syncLayerWithMB(mbMap: MbMap) { const source = mbMap.getSource(this.getId()); const mbLayerId = this._getMbLayerId(); if (!source) { const sourceDataRequest = this.getSourceDataRequest(); if (!sourceDataRequest) { - //this is possible if the layer was invisible at startup. - //the actions will not perform any data=syncing as an optimization when a layer is invisible - //when turning the layer back into visible, it's possible the url has not been resovled yet. + // this is possible if the layer was invisible at startup. + // the actions will not perform any data=syncing as an optimization when a layer is invisible + // when turning the layer back into visible, it's possible the url has not been resovled yet. return; } - const tmsSourceData = sourceDataRequest.getData(); + const tmsSourceData = sourceDataRequest.getData() as { url?: string }; if (!tmsSourceData || !tmsSourceData.url) { return; } @@ -106,9 +122,9 @@ export class TileLayer extends AbstractLayer { this._setTileLayerProperties(mbMap, mbLayerId); } - _setTileLayerProperties(mbMap, mbLayerId) { + _setTileLayerProperties(mbMap: MbMap, mbLayerId: string) { this.syncVisibilityWithMb(mbMap, mbLayerId); - mbMap.setLayerZoomRange(mbLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + mbMap.setLayerZoomRange(mbLayerId, this.getMinZoom(), this.getMaxZoom()); mbMap.setPaintProperty(mbLayerId, 'raster-opacity', this.getAlpha()); } @@ -116,7 +132,7 @@ export class TileLayer extends AbstractLayer { return 'grid'; } - isBasemap(order) { + isBasemap(order: number) { return order === 0; } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.d.ts deleted file mode 100644 index a1f49f0e1d0b3..0000000000000 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.d.ts +++ /dev/null @@ -1,13 +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 { ITileLayerArguments, TileLayer } from '../tile_layer/tile_layer'; - -export class VectorTileLayer extends TileLayer { - static type: string; - constructor(args: ITileLayerArguments); -} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx similarity index 63% rename from x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js rename to x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx index 5096e5e29bf23..2b36ecc160954 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx @@ -5,27 +5,52 @@ * 2.0. */ -import { TileLayer } from '../tile_layer/tile_layer'; +import type { Map as MbMap, Layer as MbLayer, Style as MbStyle } from '@kbn/mapbox-gl'; import _ from 'lodash'; +import { TileLayer } from '../tile_layer/tile_layer'; import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { LayerDescriptor } from '../../../../common/descriptor_types'; +import { DataRequest } from '../../util/data_request'; import { isRetina } from '../../../util'; import { addSpriteSheetToMapFromImageData, loadSpriteSheetImageData, + // @ts-expect-error } from '../../../connected_components/mb_map/utils'; +import { DataRequestContext } from '../../../actions'; +import { EMSTMSSource } from '../../sources/ems_tms_source'; + +interface SourceRequestMeta { + tileLayerId: string; +} -const MB_STYLE_TYPE_TO_OPACITY = { - fill: ['fill-opacity'], - line: ['line-opacity'], - circle: ['circle-opacity'], - background: ['background-opacity'], - symbol: ['icon-opacity', 'text-opacity'], -}; +// TODO remove once ems_client exports EmsSpriteSheet and EmsSprite type +interface EmsSprite { + height: number; + pixelRatio: number; + width: number; + x: number; + y: number; +} + +interface EmsSpriteSheet { + [spriteName: string]: EmsSprite; +} +interface SourceRequestData { + spriteSheetImageData?: ImageData; + vectorStyleSheet?: MbStyle; + spriteMeta?: { + png: string; + json: EmsSpriteSheet; + }; +} + +// TODO - rename to EmsVectorTileLayer export class VectorTileLayer extends TileLayer { static type = LAYER_TYPE.VECTOR_TILE; - static createDescriptor(options) { + static createDescriptor(options: Partial) { const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = VectorTileLayer.type; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); @@ -33,11 +58,21 @@ export class VectorTileLayer extends TileLayer { return tileLayerDescriptor; } - _canSkipSync({ prevDataRequest, nextMeta }) { + getSource(): EMSTMSSource { + return super.getSource() as EMSTMSSource; + } + + _canSkipSync({ + prevDataRequest, + nextMeta, + }: { + prevDataRequest?: DataRequest; + nextMeta: SourceRequestMeta; + }) { if (!prevDataRequest) { return false; } - const prevMeta = prevDataRequest.getMeta(); + const prevMeta = prevDataRequest.getMeta() as SourceRequestMeta; if (!prevMeta) { return false; } @@ -45,7 +80,7 @@ export class VectorTileLayer extends TileLayer { return prevMeta.tileLayerId === nextMeta.tileLayerId; } - async syncData({ startLoading, stopLoading, onLoadError }) { + async syncData({ startLoading, stopLoading, onLoadError }: DataRequestContext) { const nextMeta = { tileLayerId: this.getSource().getTileLayerId() }; const canSkipSync = this._canSkipSync({ prevDataRequest: this.getSourceDataRequest(), @@ -59,7 +94,9 @@ export class VectorTileLayer extends TileLayer { try { startLoading(SOURCE_DATA_REQUEST_ID, requestToken, nextMeta); const styleAndSprites = await this.getSource().getVectorStyleSheetAndSpriteMeta(isRetina()); - const spriteSheetImageData = await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png); + const spriteSheetImageData = styleAndSprites.spriteMeta + ? await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png) + : undefined; const data = { ...styleAndSprites, spriteSheetImageData, @@ -70,7 +107,7 @@ export class VectorTileLayer extends TileLayer { } } - _generateMbId(name) { + _generateMbId(name: string) { return `${this.getId()}_${name}`; } @@ -79,7 +116,7 @@ export class VectorTileLayer extends TileLayer { return `${this.getId()}${DELIMITTER}${this.getSource().getTileLayerId()}${DELIMITTER}`; } - _generateMbSourceId(name) { + _generateMbSourceId(name: string) { return `${this._generateMbSourceIdPrefix()}${name}`; } @@ -88,11 +125,7 @@ export class VectorTileLayer extends TileLayer { if (!sourceDataRequest) { return null; } - const vectorStyleAndSprites = sourceDataRequest.getData(); - if (!vectorStyleAndSprites) { - return null; - } - return vectorStyleAndSprites.vectorStyleSheet; + return (sourceDataRequest.getData() as SourceRequestData)?.vectorStyleSheet; } _getSpriteMeta() { @@ -100,8 +133,7 @@ export class VectorTileLayer extends TileLayer { if (!sourceDataRequest) { return null; } - const vectorStyleAndSprites = sourceDataRequest.getData(); - return vectorStyleAndSprites.spriteMeta; + return (sourceDataRequest.getData() as SourceRequestData)?.spriteMeta; } _getSpriteImageData() { @@ -109,13 +141,12 @@ export class VectorTileLayer extends TileLayer { if (!sourceDataRequest) { return null; } - const vectorStyleAndSprites = sourceDataRequest.getData(); - return vectorStyleAndSprites.spriteSheetImageData; + return (sourceDataRequest.getData() as SourceRequestData)?.spriteSheetImageData; } getMbLayerIds() { const vectorStyle = this._getVectorStyle(); - if (!vectorStyle) { + if (!vectorStyle || !vectorStyle.layers) { return []; } return vectorStyle.layers.map((layer) => this._generateMbId(layer.id)); @@ -123,29 +154,32 @@ export class VectorTileLayer extends TileLayer { getMbSourceIds() { const vectorStyle = this._getVectorStyle(); - if (!vectorStyle) { + if (!vectorStyle || !vectorStyle.sources) { return []; } const sourceIds = Object.keys(vectorStyle.sources); return sourceIds.map((sourceId) => this._generateMbSourceId(sourceId)); } - ownsMbLayerId(mbLayerId) { + ownsMbLayerId(mbLayerId: string) { return mbLayerId.startsWith(this.getId()); } - ownsMbSourceId(mbSourceId) { + ownsMbSourceId(mbSourceId: string) { return mbSourceId.startsWith(this.getId()); } - _makeNamespacedImageId(imageId) { + _makeNamespacedImageId(imageId: string) { const prefix = this.getSource().getSpriteNamespacePrefix() + '/'; return prefix + imageId; } - _requiresPrevSourceCleanup(mbMap) { + _requiresPrevSourceCleanup(mbMap: MbMap) { const sourceIdPrefix = this._generateMbSourceIdPrefix(); const mbStyle = mbMap.getStyle(); + if (!mbStyle.sources) { + return false; + } return Object.keys(mbStyle.sources).some((mbSourceId) => { const doesMbSourceBelongToLayer = this.ownsMbSourceId(mbSourceId); const doesMbSourceBelongToSource = mbSourceId.startsWith(sourceIdPrefix); @@ -153,7 +187,7 @@ export class VectorTileLayer extends TileLayer { }); } - syncLayerWithMB(mbMap) { + syncLayerWithMB(mbMap: MbMap) { const vectorStyle = this._getVectorStyle(); if (!vectorStyle) { return; @@ -162,7 +196,7 @@ export class VectorTileLayer extends TileLayer { this._removeStaleMbSourcesAndLayers(mbMap); let initialBootstrapCompleted = false; - const sourceIds = Object.keys(vectorStyle.sources); + const sourceIds = vectorStyle.sources ? Object.keys(vectorStyle.sources) : []; sourceIds.forEach((sourceId) => { if (initialBootstrapCompleted) { return; @@ -170,20 +204,20 @@ export class VectorTileLayer extends TileLayer { const mbSourceId = this._generateMbSourceId(sourceId); const mbSource = mbMap.getSource(mbSourceId); if (mbSource) { - //if a single source is present, the layer already has bootstrapped with the mbMap + // if a single source is present, the layer already has bootstrapped with the mbMap initialBootstrapCompleted = true; return; } - mbMap.addSource(mbSourceId, vectorStyle.sources[sourceId]); + mbMap.addSource(mbSourceId, vectorStyle.sources![sourceId]); }); if (!initialBootstrapCompleted) { - //sync spritesheet + // sync spritesheet const spriteMeta = this._getSpriteMeta(); if (!spriteMeta) { return; } - const newJson = {}; + const newJson: EmsSpriteSheet = {}; for (const imageId in spriteMeta.json) { if (spriteMeta.json.hasOwnProperty(imageId)) { const namespacedImageId = this._makeNamespacedImageId(imageId); @@ -197,8 +231,9 @@ export class VectorTileLayer extends TileLayer { } addSpriteSheetToMapFromImageData(newJson, imageData, mbMap); - //sync layers - vectorStyle.layers.forEach((layer) => { + // sync layers + const layers = vectorStyle.layers ? vectorStyle.layers : []; + layers.forEach((layer) => { const mbLayerId = this._generateMbId(layer.id); const mbLayer = mbMap.getLayer(mbLayerId); if (mbLayer) { @@ -206,7 +241,10 @@ export class VectorTileLayer extends TileLayer { } const newLayerObject = { ...layer, - source: this._generateMbSourceId(layer.source), + source: + typeof (layer as MbLayer).source === 'string' + ? this._generateMbSourceId((layer as MbLayer).source as string) + : undefined, id: mbLayerId, }; @@ -237,15 +275,35 @@ export class VectorTileLayer extends TileLayer { this._setTileLayerProperties(mbMap); } - _setOpacityForType(mbMap, mbLayer, mbLayerId) { - const opacityProps = MB_STYLE_TYPE_TO_OPACITY[mbLayer.type]; - if (!opacityProps) { - return; + _getOpacityProps(layerType: string): string[] { + if (layerType === 'fill') { + return ['fill-opacity']; + } + + if (layerType === 'line') { + return ['line-opacity']; + } + + if (layerType === 'circle') { + return ['circle-opacity']; } - opacityProps.forEach((opacityProp) => { - if (mbLayer.paint && typeof mbLayer.paint[opacityProp] === 'number') { - const newOpacity = mbLayer.paint[opacityProp] * this.getAlpha(); + if (layerType === 'background') { + return ['background-opacity']; + } + + if (layerType === 'symbol') { + return ['icon-opacity', 'text-opacity']; + } + + return []; + } + + _setOpacityForType(mbMap: MbMap, mbLayer: MbLayer, mbLayerId: string) { + this._getOpacityProps(mbLayer.type).forEach((opacityProp) => { + const mbPaint = mbLayer.paint as { [key: string]: unknown } | undefined; + if (mbPaint && typeof mbPaint[opacityProp] === 'number') { + const newOpacity = (mbPaint[opacityProp] as number) * this.getAlpha(); mbMap.setPaintProperty(mbLayerId, opacityProp, newOpacity); } else { mbMap.setPaintProperty(mbLayerId, opacityProp, this.getAlpha()); @@ -253,21 +311,21 @@ export class VectorTileLayer extends TileLayer { }); } - _setLayerZoomRange(mbMap, mbLayer, mbLayerId) { - let minZoom = this._descriptor.minZoom; + _setLayerZoomRange(mbMap: MbMap, mbLayer: MbLayer, mbLayerId: string) { + let minZoom = this.getMinZoom(); if (typeof mbLayer.minzoom === 'number') { minZoom = Math.max(minZoom, mbLayer.minzoom); } - let maxZoom = this._descriptor.maxZoom; + let maxZoom = this.getMaxZoom(); if (typeof mbLayer.maxzoom === 'number') { maxZoom = Math.min(maxZoom, mbLayer.maxzoom); } mbMap.setLayerZoomRange(mbLayerId, minZoom, maxZoom); } - _setTileLayerProperties(mbMap) { + _setTileLayerProperties(mbMap: MbMap) { const vectorStyle = this._getVectorStyle(); - if (!vectorStyle) { + if (!vectorStyle || !vectorStyle.layers) { return; } From a14782a15ee2c3f93ecdcf690604b6fac859f4bc Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 8 Nov 2021 19:29:52 -0800 Subject: [PATCH 18/98] [Alerting UI] Actions menu with mute/unmute, disable/enable should be available for the rules with requiresAppContext (#117806) * [Alerting UI] Actions menu with mute/unmute, disable/enable sould be available for the rules with requiresAppContext * fixed test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/alerts_list.test.tsx | 32 ++++++-- .../alerts_list/components/alerts_list.tsx | 77 ++++++++++--------- .../apps/triggers_actions_ui/alerts_list.ts | 16 ++++ 3 files changed, 83 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 53f5e25530e98..5067cbbee9bf4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -490,7 +490,7 @@ describe('alerts_list component with items', () => { it('does not render edit and delete button when rule type does not allow editing in rules management', async () => { await setup(false); expect(wrapper.find('[data-test-subj="alertSidebarEditAction"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeTruthy(); }); }); @@ -540,7 +540,7 @@ describe('alerts_list component empty with show only capability', () => { describe('alerts_list with show only capability', () => { let wrapper: ReactWrapper; - async function setup() { + async function setup(editable: boolean = true) { loadAlerts.mockResolvedValue({ page: 1, perPage: 10000, @@ -606,7 +606,20 @@ describe('alerts_list with show only capability', () => { loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); - ruleTypeRegistry.has.mockReturnValue(false); + const ruleTypeMock: AlertTypeModel = { + id: 'test_alert_type', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; + + ruleTypeRegistry.has.mockReturnValue(true); + ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; @@ -621,18 +634,27 @@ describe('alerts_list with show only capability', () => { } it('renders table of alerts with edit button disabled', async () => { - await setup(); + await setup(false); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); expect(wrapper.find('[data-test-subj="editActionHoverButton"]')).toHaveLength(0); }); it('renders table of alerts with delete button disabled', async () => { - await setup(); + const { hasAllPrivilege } = jest.requireMock('../../../lib/capabilities'); + hasAllPrivilege.mockReturnValue(false); + await setup(false); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0); }); + + it('renders table of alerts with actions menu collapsedItemActions', async () => { + await setup(false); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="collapsedItemActions"]').length).toBeGreaterThan(0); + }); }); describe('alerts_list with disabled itmes', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index a7d54a896d7ef..b48d5a6a3629f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -571,47 +571,50 @@ export const AlertsList: React.FunctionComponent = () => { name: '', width: '10%', render(item: AlertTableItem) { - return item.isEditable && isRuleTypeEditableInContext(item.alertTypeId) ? ( + return ( - - onRuleEdit(item)} - iconType={'pencil'} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel', - { defaultMessage: 'Edit' } - )} - /> - - - setAlertsToDelete([item.id])} - iconType={'trash'} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel', - { defaultMessage: 'Delete' } - )} - /> - + {item.isEditable && isRuleTypeEditableInContext(item.alertTypeId) ? ( + + onRuleEdit(item)} + iconType={'pencil'} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel', + { defaultMessage: 'Edit' } + )} + /> + + ) : null} + {item.isEditable ? ( + + setAlertsToDelete([item.id])} + iconType={'trash'} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel', + { defaultMessage: 'Delete' } + )} + /> + + ) : null} - { /> - ) : null; + ); }, }, ]; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index dede481669664..eb4c0fbe425c4 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -206,6 +206,22 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); + it('should be able to mute the rule with non "alerts" consumer from a non editable context', async () => { + const createdAlert = await createAlert({ consumer: 'siem' }); + await refreshAlertsList(); + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); + + await testSubjects.click('collapsedItemActions'); + + await testSubjects.click('muteButton'); + + await retry.tryForTime(30000, async () => { + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); + const muteBadge = await testSubjects.find('mutedActionsBadge'); + expect(await muteBadge.isDisplayed()).to.eql(true); + }); + }); + it('should unmute single alert', async () => { const createdAlert = await createAlert(); await muteAlert(createdAlert.id); From c3d81c6427e692f26ff81c3c07ba81675ddce14d Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 8 Nov 2021 21:18:43 -0700 Subject: [PATCH 19/98] [Reporting] Use non-deprecated csv_searchsource API in integration tests (#117456) * [Reporting] Use non-deprecated csv_searchsource API in integration tests * update snapshot * fix tests * update usage tests * test organization Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ilm_migration_apis.ts | 23 ++++-- .../reporting_and_security/spaces.ts | 78 ++++++++++++++----- .../reporting_and_security/usage.ts | 57 +++++++------- .../services/generation_urls.ts | 2 - 4 files changed, 104 insertions(+), 56 deletions(-) diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts index af6afe99e8c9d..b5a7457912278 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { JOB_PARAMS_RISON_CSV_DEPRECATED } from '../services/fixtures'; import { FtrProviderContext } from '../ftr_provider_context'; import { ILM_POLICY_NAME } from '../../../plugins/reporting/common/constants'; @@ -19,6 +18,16 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const security = getService('security'); + const JOB_PARAMS_RISON_CSV = + `(columns:!(order_date,category,customer_full_name,taxful_total_price,currency)` + + `,objectType:search,searchSource:(fields:!((field:'*',include_unmapped:true))` + + `,filter:!((meta:(field:order_date,index:aac3e500-f2c7-11ea-8250-fb138aa491e7` + + `,params:()),query:(range:(order_date:(format:strict_date_optional_time` + + `,gte:'2019-06-02T12:28:40.866Z',lte:'2019-07-18T20:59:57.136Z')))))` + + `,index:aac3e500-f2c7-11ea-8250-fb138aa491e7,parent:(filter:!(),highlightAll:!t` + + `,index:aac3e500-f2c7-11ea-8250-fb138aa491e7,query:(language:kuery,query:'')` + + `,version:!t),sort:!((order_date:desc)),trackTotalHits:!t),title:'EC SEARCH from DEFAULT')`; + describe('ILM policy migration APIs', () => { before(async () => { await reportingAPI.initLogs(); @@ -39,9 +48,9 @@ export default function ({ getService }: FtrProviderContext) { // try creating a report await supertest - .post(`/api/reporting/generate/csv`) + .post(`/api/reporting/generate/csv_searchsource`) .set('kbn-xsrf', 'xxx') - .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + .send({ jobParams: JOB_PARAMS_RISON_CSV }); expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('ok'); }); @@ -51,9 +60,9 @@ export default function ({ getService }: FtrProviderContext) { await es.ilm.deleteLifecycle({ name: ILM_POLICY_NAME }); await supertest - .post(`/api/reporting/generate/csv`) + .post(`/api/reporting/generate/csv_searchsource`) .set('kbn-xsrf', 'xxx') - .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + .send({ jobParams: JOB_PARAMS_RISON_CSV }); expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('policy-not-found'); // assert that migration fixes this @@ -64,9 +73,9 @@ export default function ({ getService }: FtrProviderContext) { it('detects when reporting indices should be migrated due to unmanaged indices', async () => { await reportingAPI.makeAllReportingIndicesUnmanaged(); await supertest - .post(`/api/reporting/generate/csv`) + .post(`/api/reporting/generate/csv_searchsource`) .set('kbn-xsrf', 'xxx') - .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); + .send({ jobParams: JOB_PARAMS_RISON_CSV }); expect(await reportingAPI.checkIlmMigrationStatus()).to.eql('indices-not-managed-by-policy'); // assert that migration fixes this diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts index e1ca664122c76..78858239ddec9 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts @@ -53,16 +53,38 @@ export default function ({ getService }: FtrProviderContext) { await esArchiver.unload(spacesSharedObjectsArchive); }); + /* + * NOTE: All timestamps in the documents are midnight UTC. + * "00:00:00.000" means the time is formatted in UTC timezone + */ describe('CSV saved search export', () => { + const JOB_PARAMS_CSV_DEFAULT_SPACE = + `columns:!(order_date,category,customer_full_name,taxful_total_price,currency),objectType:search,searchSource:(fields:!((field:'*',include_unmapped:true))` + + `,filter:!((meta:(field:order_date,index:aac3e500-f2c7-11ea-8250-fb138aa491e7,params:()),query:(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-02T12:28:40.866Z'` + + `,lte:'2019-07-18T20:59:57.136Z'))))),index:aac3e500-f2c7-11ea-8250-fb138aa491e7,parent:(filter:!(),highlightAll:!t,index:aac3e500-f2c7-11ea-8250-fb138aa491e7` + + `,query:(language:kuery,query:''),version:!t),sort:!((order_date:desc)),trackTotalHits:!t)`; + + const JOB_PARAMS_CSV_NONDEFAULT_SPACE = + `columns:!(order_date,category,customer_full_name,taxful_total_price,currency),objectType:search,searchSource:(fields:!((field:'*',include_unmapped:true))` + + `,filter:!((meta:(field:order_date,index:afac7364-c755-5f5c-acd5-8ed6605c5c77,params:()),query:(range:(order_date:(format:strict_date_optional_time` + + `,gte:'2006-11-04T19:58:58.244Z',lte:'2021-11-04T18:58:58.244Z'))))),index:afac7364-c755-5f5c-acd5-8ed6605c5c77,parent:(filter:!(),highlightAll:!t` + + `,index:afac7364-c755-5f5c-acd5-8ed6605c5c77,query:(language:kuery,query:''),version:!t),sort:!((order_date:desc)),trackTotalHits:!t)`; + it('should use formats from the default space', async () => { kibanaServer.uiSettings.update({ 'csv:separator': ',', 'dateFormat:tz': 'UTC' }); - const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, { - jobParams: `(conflictedTypesFields:!(),fields:!(order_date,order_date,customer_full_name,taxful_total_price),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,customer_full_name,taxful_total_price)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T04:49:43.495Z',lte:'2019-07-14T10:25:34.149Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,customer_full_name,taxful_total_price),version:!t),index:'ec*'),title:'EC SEARCH')`, + const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv_searchsource`, { + jobParams: `(${JOB_PARAMS_CSV_DEFAULT_SPACE},title:'EC SEARCH')`, }); const csv = await getCompleted$(path).toPromise(); - expect(csv).to.match( - /^"order_date","order_date","customer_full_name","taxful_total_price"\n"Jul 12, 2019 @ 00:00:00.000","Jul 12, 2019 @ 00:00:00.000","Sultan Al Boone","173.96"/ - ); + + expectSnapshot(csv.slice(0, 500)).toMatchInline(` + "\\"order_date\\",category,\\"customer_full_name\\",\\"taxful_total_price\\",currency + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Sultan Al Boone\\",174,EUR + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Shoes, Women's Clothing\\",\\"Pia Richards\\",\\"41.969\\",EUR + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",\\"Brigitte Meyer\\",\\"40.969\\",EUR + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",\\"Abd Mccarthy\\",\\"41.969\\",EUR + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",\\"Robert " + `); }); it('should use formats from non-default spaces', async () => { @@ -72,15 +94,21 @@ export default function ({ getService }: FtrProviderContext) { 'dateFormat:tz': 'US/Alaska', }); const path = await reportingAPI.postJobJSON( - `/s/non_default_space/api/reporting/generate/csv`, + `/s/non_default_space/api/reporting/generate/csv_searchsource`, { - jobParams: `(conflictedTypesFields:!(),fields:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency),indexPatternId:'067dec90-e7ee-11ea-a730-d58e9ea7581b',metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T08:24:16.425Z',lte:'2019-07-13T09:31:07.520Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency),version:!t),index:'ecommerce*'),title:'Ecom Search')`, + jobParams: `(${JOB_PARAMS_CSV_NONDEFAULT_SPACE},title:'Ecom Search from Non-Default')`, } ); const csv = await getCompleted$(path).toPromise(); - expect(csv).to.match( - /^order_date;category;customer_first_name;customer_full_name;total_quantity;total_unique_products;taxless_total_price;taxful_total_price;currency\nJul 11, 2019 @ 16:00:00.000;/ - ); + expectSnapshot(csv.slice(0, 500)).toMatchInline(` + "order_date;category;customer_full_name;taxful_total_price;currency + Jul 11, 2019 @ 16:00:00.000;Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories;Sultan Al Boone;174;EUR + Jul 11, 2019 @ 16:00:00.000;Women's Shoes, Women's Clothing;Pia Richards;41.969;EUR + Jul 11, 2019 @ 16:00:00.000;Women's Clothing;Brigitte Meyer;40.969;EUR + Jul 11, 2019 @ 16:00:00.000;Men's Clothing;Abd Mccarthy;41.969;EUR + Jul 11, 2019 @ 16:00:00.000;Men's Clothing;Robert Banks;36.969;EUR + Jul 11, 2019 @ 16:00:00." + `); }); it(`should use browserTimezone in jobParams for date formatting`, async () => { @@ -90,25 +118,35 @@ export default function ({ getService }: FtrProviderContext) { 'csv:separator': ';', 'dateFormat:tz': tzSettings, }); - const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, { - jobParams: `(browserTimezone:${tzParam},conflictedTypesFields:!(),fields:!(order_date,category,customer_full_name,taxful_total_price,currency),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,category,customer_full_name,taxful_total_price,currency)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-05-30T05:09:59.743Z',lte:'2019-07-26T08:47:09.682Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,category,customer_full_name,taxful_total_price,currency),version:!t),index:'ec*'),title:'EC SEARCH from DEFAULT')`, + const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv_searchsource`, { + jobParams: `(browserTimezone:${tzParam},${JOB_PARAMS_CSV_DEFAULT_SPACE},title:'EC SEARCH')`, }); const csv = await getCompleted$(path).toPromise(); - expect(csv).to.match( - /^"order_date",category,"customer_full_name","taxful_total_price",currency\n"Jul 11, 2019 @ 17:00:00.000"/ - ); + expectSnapshot(csv.slice(0, 500)).toMatchInline(` + "\\"order_date\\",category,\\"customer_full_name\\",\\"taxful_total_price\\",currency + \\"Jul 11, 2019 @ 17:00:00.000\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Sultan Al Boone\\",174,EUR + \\"Jul 11, 2019 @ 17:00:00.000\\",\\"Women's Shoes, Women's Clothing\\",\\"Pia Richards\\",\\"41.969\\",EUR + \\"Jul 11, 2019 @ 17:00:00.000\\",\\"Women's Clothing\\",\\"Brigitte Meyer\\",\\"40.969\\",EUR + \\"Jul 11, 2019 @ 17:00:00.000\\",\\"Men's Clothing\\",\\"Abd Mccarthy\\",\\"41.969\\",EUR + \\"Jul 11, 2019 @ 17:00:00.000\\",\\"Men's Clothing\\",\\"Robert " + `); }); it(`should default to UTC for date formatting when timezone is not known`, async () => { kibanaServer.uiSettings.update({ 'csv:separator': ',', 'dateFormat:tz': 'Browser' }); - const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, { - jobParams: `(conflictedTypesFields:!(),fields:!(order_date,order_date,customer_full_name,taxful_total_price),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,customer_full_name,taxful_total_price)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T04:49:43.495Z',lte:'2019-07-14T10:25:34.149Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,customer_full_name,taxful_total_price),version:!t),index:'ec*'),title:'EC SEARCH')`, + const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv_searchsource`, { + jobParams: `(${JOB_PARAMS_CSV_DEFAULT_SPACE},title:'EC SEARCH')`, }); const csv = await getCompleted$(path).toPromise(); - expect(csv).to.match( - /^"order_date","order_date","customer_full_name","taxful_total_price"\n"Jul 12, 2019 @ 00:00:00.000","Jul 12, 2019 @ 00:00:00.000","Sultan Al Boone","173.96"/ - ); + expectSnapshot(csv.slice(0, 500)).toMatchInline(` + "\\"order_date\\",category,\\"customer_full_name\\",\\"taxful_total_price\\",currency + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Sultan Al Boone\\",174,EUR + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Shoes, Women's Clothing\\",\\"Pia Richards\\",\\"41.969\\",EUR + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",\\"Brigitte Meyer\\",\\"40.969\\",EUR + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",\\"Abd Mccarthy\\",\\"41.969\\",EUR + \\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",\\"Robert " + `); }); }); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts index 221d345ac2d5b..4f17d753faf0e 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts @@ -10,6 +10,11 @@ import { FtrProviderContext } from '../ftr_provider_context'; import * as GenerationUrls from '../services/generation_urls'; import { ReportingUsageStats } from '../services/usage'; +const JOB_PARAMS_CSV_DEFAULT_SPACE = + `columns:!(order_date,category,customer_full_name,taxful_total_price,currency),objectType:search,searchSource:(fields:!((field:'*',include_unmapped:true))` + + `,filter:!((meta:(field:order_date,index:aac3e500-f2c7-11ea-8250-fb138aa491e7,params:()),query:(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-02T12:28:40.866Z'` + + `,lte:'2019-07-18T20:59:57.136Z'))))),index:aac3e500-f2c7-11ea-8250-fb138aa491e7,parent:(filter:!(),highlightAll:!t,index:aac3e500-f2c7-11ea-8250-fb138aa491e7` + + `,query:(language:kuery,query:''),version:!t),sort:!((order_date:desc)),trackTotalHits:!t)`; const OSS_KIBANA_ARCHIVE_PATH = 'test/functional/fixtures/es_archiver/dashboard/current/kibana'; const OSS_DATA_ARCHIVE_PATH = 'test/functional/fixtures/es_archiver/dashboard/current/data'; @@ -20,30 +25,14 @@ interface UsageStats { // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); const reportingAPI = getService('reportingAPI'); const retry = getService('retry'); const usageAPI = getService('usageAPI'); describe('Usage', () => { - before(async () => { - await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); - await esArchiver.load(OSS_DATA_ARCHIVE_PATH); - - await kibanaServer.uiSettings.update({ - defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', - }); - await reportingAPI.deleteAllReports(); - }); - - after(async () => { - await esArchiver.unload(OSS_KIBANA_ARCHIVE_PATH); - await esArchiver.unload(OSS_DATA_ARCHIVE_PATH); - }); - - afterEach(async () => { - await reportingAPI.deleteAllReports(); - }); + const deleteAllReports = () => reportingAPI.deleteAllReports(); + beforeEach(deleteAllReports); + after(deleteAllReports); describe('initial state', () => { let usage: UsageStats; @@ -69,8 +58,8 @@ export default function ({ getService }: FtrProviderContext) { reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 0); reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 0); reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 0); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv', 0); - reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'csv', 0); + reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 0); + reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'csv_searchsource', 0); reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 0); reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'printable_pdf', 0); }); @@ -123,10 +112,24 @@ export default function ({ getService }: FtrProviderContext) { }); describe('from new jobs posted', () => { - it('should handle csv', async () => { + before(async () => { + await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); + await esArchiver.load(OSS_DATA_ARCHIVE_PATH); + await reportingAPI.initEcommerce(); + }); + + after(async () => { + await esArchiver.unload(OSS_KIBANA_ARCHIVE_PATH); + await esArchiver.unload(OSS_DATA_ARCHIVE_PATH); + await reportingAPI.teardownEcommerce(); + }); + + it('should handle csv_searchsource', async () => { await reportingAPI.expectAllJobsToFinishSuccessfully( await Promise.all([ - reportingAPI.postJob(GenerationUrls.CSV_DISCOVER_KUERY_AND_FILTER_6_3), + reportingAPI.postJob( + `/api/reporting/generate/csv_searchsource?jobParams=(${JOB_PARAMS_CSV_DEFAULT_SPACE})` + ), ]) ); @@ -135,7 +138,7 @@ export default function ({ getService }: FtrProviderContext) { reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 0); reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 0); reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 0); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv', 1); + reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 1); reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 0); }); @@ -152,7 +155,7 @@ export default function ({ getService }: FtrProviderContext) { reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 1); reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 2); reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 0); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv', 0); + reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 0); reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2); }); @@ -171,14 +174,14 @@ export default function ({ getService }: FtrProviderContext) { reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 1); reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 0); reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 2); - reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv', 0); + reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 0); reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2); reportingAPI.expectAllTimePdfAppStats(usage, 'visualization', 1); reportingAPI.expectAllTimePdfAppStats(usage, 'dashboard', 1); reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 0); reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 2); - reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'csv', 0); + reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'csv_searchsource', 0); reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'printable_pdf', 2); }); }); diff --git a/x-pack/test/reporting_api_integration/services/generation_urls.ts b/x-pack/test/reporting_api_integration/services/generation_urls.ts index f6379bc376e76..d5cf96d911628 100644 --- a/x-pack/test/reporting_api_integration/services/generation_urls.ts +++ b/x-pack/test/reporting_api_integration/services/generation_urls.ts @@ -20,8 +20,6 @@ export const PDF_PRESERVE_PIE_VISUALIZATION_6_3 = '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(dimensions:(height:441,width:1002),id:preserve_layout),objectType:visualization,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fvisualize%2Fedit%2F3fe22200-3dcb-11e8-8660-4d65aa086b3c%3F_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(filters:!!(),linked:!!f,query:(language:lucene,query:!%27!%27),uiState:(),vis:(aggs:!!((enabled:!!t,id:!%271!%27,params:(),schema:metric,type:count),(enabled:!!t,id:!%272!%27,params:(field:bytes,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!%271!%27,otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!%27Rendering%2BTest:%2Bpie!%27,type:pie))%27),title:%27Rendering%20Test:%20pie%27)'; export const PDF_PRINT_PIE_VISUALIZATION_FILTER_AND_SAVED_SEARCH_6_3 = '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(id:print),objectType:visualization,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fvisualize%2Fedit%2Fbefdb6b0-3e59-11e8-9fc3-39e49624228e%3F_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(filters:!!((!%27$state!%27:(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!%27!%27),uiState:(),vis:(aggs:!!((enabled:!!t,id:!%271!%27,params:(),schema:metric,type:count),(enabled:!!t,id:!%272!%27,params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!%271!%27,otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!%27Filter%2BTest:%2Banimals:%2Blinked%2Bto%2Bsearch%2Bwith%2Bfilter!%27,type:pie))%27),title:%27Filter%20Test:%20animals:%20linked%20to%20search%20with%20filter%27)'; -export const CSV_DISCOVER_KUERY_AND_FILTER_6_3 = - '/api/reporting/generate/csv?jobParams=(conflictedTypesFields:!(),fields:!(%27@timestamp%27,agent,bytes,clientip),indexPatternId:%270bf35f60-3dc9-11e8-8660-4d65aa086b3c%27,metaFields:!(_source,_id,_type,_index,_score),searchRequest:(body:(_source:(excludes:!(),includes:!(%27@timestamp%27,agent,bytes,clientip)),docvalue_fields:!(%27@timestamp%27),query:(bool:(filter:!((bool:(minimum_should_match:1,should:!((match:(clientip:%2773.14.212.83%27)))))),must:!((range:(bytes:(gte:100,lt:1000))),(range:(%27@timestamp%27:(format:epoch_millis,gte:1369165215770,lte:1526931615770)))),must_not:!(),should:!())),script_fields:(),sort:!((%27@timestamp%27:(order:desc,unmapped_type:boolean))),stored_fields:!(%27@timestamp%27,agent,bytes,clientip),version:!t),index:%27logstash-*%27),title:%27Bytes%20and%20kuery%20in%20saved%20search%20with%20filter%27,type:search)'; export const PDF_PRINT_DASHBOARD_6_2 = '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(id:print),objectType:dashboard,queryString:%27_g%3D(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!%27Mon%2BApr%2B09%2B2018%2B17:56:08%2BGMT-0400!%27,mode:absolute,to:!%27Wed%2BApr%2B11%2B2018%2B17:56:08%2BGMT-0400!%27))%26_a%3D(description:!%27!%27,filters:!!((!%27$state!%27:(store:appState),meta:(alias:!!n,disabled:!!f,field:isDog,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:isDog,negate:!!f,params:(value:!!t),type:phrase,value:true),script:(script:(inline:!%27boolean%2Bcompare(Supplier%2Bs,%2Bdef%2Bv)%2B%257Breturn%2Bs.get()%2B%253D%253D%2Bv%3B%257Dcompare(()%2B-%253E%2B%257B%2Breturn%2Bdoc%255B!!!%27animal.keyword!!!%27%255D.value%2B%253D%253D%2B!!!%27dog!!!%27%2B%257D,%2Bparams.value)%3B!%27,lang:painless,params:(value:!!t))))),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((gridData:(h:3,i:!%274!%27,w:6,x:6,y:0),id:edb65990-53ca-11e8-b481-c9426d020fcd,panelIndex:!%274!%27,type:visualization,version:!%276.2.4!%27),(gridData:(h:3,i:!%275!%27,w:6,x:0,y:0),id:!%270644f890-53cb-11e8-b481-c9426d020fcd!%27,panelIndex:!%275!%27,type:visualization,version:!%276.2.4!%27)),query:(language:lucene,query:!%27weightLbs:%253E15!%27),timeRestore:!!t,title:!%27Animal%2BWeights%2B(created%2Bin%2B6.2)!%27,viewMode:view)%27,savedObjectId:%271b2f47b0-53cb-11e8-b481-c9426d020fcd%27)'; From a34d3d10dd5611aa7712bad7f98eecb08e55a0e2 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 9 Nov 2021 07:09:39 +0100 Subject: [PATCH 20/98] Fix wrongly suggested module paths for @kbn/eslint/module_migration rule (#117781) * Use an alternative KIBANA_ROOT if bazel cache detected * Use several fallbacks to find the kibana root * Update comments * Add test for relative import conversion * Improve comments * remove console log * Improve comments * Add second case * improve tests filenames --- .../helpers/find_kibana_root.js | 49 +++++++++++++ .../rules/module_migration.js | 3 +- .../rules/module_migration.test.js | 71 +++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 packages/kbn-eslint-plugin-eslint/helpers/find_kibana_root.js diff --git a/packages/kbn-eslint-plugin-eslint/helpers/find_kibana_root.js b/packages/kbn-eslint-plugin-eslint/helpers/find_kibana_root.js new file mode 100644 index 0000000000000..5915b10b443bb --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/helpers/find_kibana_root.js @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +const path = require('path'); +const fs = require('fs'); + +function isKibanaRoot(maybeKibanaRoot) { + try { + const packageJsonPath = path.join(maybeKibanaRoot, 'package.json'); + fs.accessSync(packageJsonPath, fs.constants.R_OK); + const packageJsonContent = fs.readFileSync(packageJsonPath); + return JSON.parse(packageJsonContent).name === 'kibana'; + } catch (e) { + return false; + } +} + +module.exports = function findKibanaRoot() { + let maybeKibanaRoot = path.resolve(__dirname, '../../..'); + + // when using syslinks, __dirname reports outside of the repo + // if that's the case, the path will contain .cache/bazel + if (!maybeKibanaRoot.includes('.cache/bazel')) { + return maybeKibanaRoot; + } + + // process.argv[1] would be the eslint binary, a correctly-set editor + // will use a local eslint inside the repo node_modules and its value + // should be `ACTUAL_KIBANA_ROOT/node_modules/.bin/eslint` + maybeKibanaRoot = path.resolve(process.argv[1], '../../../'); + if (isKibanaRoot(maybeKibanaRoot)) { + return maybeKibanaRoot; + } + + // eslint should run on the repo root level + // try to use process.cwd as the kibana root + maybeKibanaRoot = process.cwd(); + if (isKibanaRoot(maybeKibanaRoot)) { + return maybeKibanaRoot; + } + + // fallback to the first predicted path (original script) + return maybeKibanaRoot; +}; diff --git a/packages/kbn-eslint-plugin-eslint/rules/module_migration.js b/packages/kbn-eslint-plugin-eslint/rules/module_migration.js index 3175210eccb10..04fbbfd35a565 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/module_migration.js +++ b/packages/kbn-eslint-plugin-eslint/rules/module_migration.js @@ -7,7 +7,8 @@ */ const path = require('path'); -const KIBANA_ROOT = path.resolve(__dirname, '../../..'); +const findKibanaRoot = require('../helpers/find_kibana_root'); +const KIBANA_ROOT = findKibanaRoot(); function checkModuleNameNode(context, mappings, node, desc = 'Imported') { const mapping = mappings.find( diff --git a/packages/kbn-eslint-plugin-eslint/rules/module_migration.test.js b/packages/kbn-eslint-plugin-eslint/rules/module_migration.test.js index 1ff65fc19a966..2ecaf283133e7 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/module_migration.test.js +++ b/packages/kbn-eslint-plugin-eslint/rules/module_migration.test.js @@ -77,5 +77,76 @@ ruleTester.run('@kbn/eslint/module-migration', rule, { export const foo2 = 'bar' `, }, + /** + * Given this tree: + * x-pack/ + * - common/ + * - foo.ts <-- the target import + * - other/ + * - folder/ + * - bar.ts <-- the linted fle + * import "x-pack/common/foo" should be + * import ../../foo + */ + { + code: dedent` + import "x-pack/common/foo" + `, + filename: 'x-pack/common/other/folder/bar.ts', + options: [ + [ + { + from: 'x-pack', + to: 'foo', + toRelative: 'x-pack', + }, + ], + ], + errors: [ + { + line: 1, + message: 'Imported module "x-pack/common/foo" should be "../../foo"', + }, + ], + output: dedent` + import '../../foo' + `, + }, + /** + * Given this tree: + * x-pack/ + * - common/ + * - foo.ts <-- the target import + * - another/ + * - posible + * - example <-- the linted file + * + * import "x-pack/common/foo" should be + * import ../../common/foo + */ + { + code: dedent` + import "x-pack/common/foo" + `, + filename: 'x-pack/another/possible/example.ts', + options: [ + [ + { + from: 'x-pack', + to: 'foo', + toRelative: 'x-pack', + }, + ], + ], + errors: [ + { + line: 1, + message: 'Imported module "x-pack/common/foo" should be "../../common/foo"', + }, + ], + output: dedent` + import '../../common/foo' + `, + }, ], }); From f9c982ddc2b56e9ec17a3e90a6df973885bfd567 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 9 Nov 2021 10:26:51 +0100 Subject: [PATCH 21/98] [ML] APM Correlations: Fix usage in load balancing/HA setups. (#115145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The way we customized the use of search strategies caused issues with race conditions when multiple Kibana instances were used for load balancing. This PR migrates away from search strategies and uses regular APM API endpoints. - The task that manages calling the sequence of queries to run the correlations analysis is now in a custom React hook (useFailedTransactionsCorrelations / useLatencyCorrelations) instead of a task on the Kibana server side. While they show up as new lines/files in the git diff, the code for the hooks is more or less a combination of the previous useSearchStrategy and the server side service files that managed queries and state. - The consuming React UI components only needed minimal changes. The above mentioned hooks return the same data structure as the previously used useSearchStrategy. This also means functional UI tests didn't need any changes and should pass as is. - API integration tests have been added for the individual new endpoints. The test files that were previously used for the search strategies are still there to simulate a full analysis run, the assertions for the resulting data have the same values, it's just the structure that had to be adapted. - Previously all ES queries of the analysis were run sequentially. The new endpoints run ES queries in parallel where possible. Chunking is managed in the hooks on the client side. - For now the endpoints use the standard current user's esClient. I tried to use the APM client, but it was missing a wrapper for the fieldCaps method and I ran into a problem when trying to construct a random_score query. Sticking to the esClient allowed to leave most of the functions that run the actual queries unchanged. If possible I'd like to pick this up in a follow up. All the endpoints still use withApmSpan() now though. - The previous use of generators was also refactored away, as mentioned above, the queries are now run in parallel. Because we might run up to hundreds of similar requests for correlation analysis, we don't want the analysis to fail if just a single query fails like we did in the previous search strategy based task. I created a util splitAllSettledPromises() to handle Promise.allSettled() and split the results and errors to make the handling easier. Better naming suggestions are welcome 😅 . A future improvement could be to not run individual queries but combine them into nested aggs or using msearch. That's out of scope for this PR though. --- .../correlations}/constants.ts | 8 +- .../constants.ts | 0 .../failed_transactions_correlations/types.ts | 8 +- .../field_stats_types.ts | 4 +- .../latency_correlations/types.ts | 18 +- .../types.ts | 23 +- .../get_prioritized_field_value_pairs.test.ts | 0 .../get_prioritized_field_value_pairs.ts | 4 +- .../utils/has_prefix_to_include.test.ts | 0 .../utils/has_prefix_to_include.ts | 0 .../correlations/utils}/index.ts | 3 +- .../apm/common/search_strategies/constants.ts | 15 - .../context_popover/context_popover.tsx | 2 +- .../context_popover/top_values.tsx | 2 +- .../app/correlations/correlations_log.tsx | 38 -- .../app/correlations/correlations_table.tsx | 2 +- .../failed_transactions_correlations.tsx | 53 +-- .../latency_correlations.test.tsx | 11 +- .../app/correlations/latency_correlations.tsx | 42 +- ..._failed_transactions_correlations.test.tsx | 399 ++++++++++++++++++ .../use_failed_transactions_correlations.ts | 257 +++++++++++ .../app/correlations/use_fetch_params.ts | 51 +++ .../use_latency_correlations.test.tsx | 360 ++++++++++++++++ .../correlations/use_latency_correlations.ts | 275 ++++++++++++ .../correlations/utils/analysis_hook_utils.ts | 40 ++ ...nsactions_correlation_impact_label.test.ts | 2 +- ...d_transactions_correlation_impact_label.ts | 4 +- .../utils/get_overall_histogram.test.ts | 10 +- .../utils/get_overall_histogram.ts | 4 +- .../distribution/index.tsx | 10 +- ...use_transaction_distribution_chart_data.ts | 73 +--- .../index.test.tsx | 2 +- .../transaction_distribution_chart/index.tsx | 2 +- .../apm/public/hooks/use_search_strategy.ts | 218 ---------- .../field_stats/get_boolean_field_stats.ts | 4 +- .../field_stats/get_field_stats.test.ts | 0 .../queries/field_stats/get_fields_stats.ts | 14 +- .../field_stats/get_keyword_field_stats.ts | 8 +- .../field_stats/get_numeric_field_stats.ts | 4 +- .../queries/get_filters.ts | 4 +- .../queries/get_query_with_params.test.ts | 0 .../queries/get_query_with_params.ts | 6 +- .../queries/get_request_base.test.ts | 0 .../queries/get_request_base.ts | 4 +- .../queries/index.ts | 4 +- .../queries/query_correlation.test.ts | 0 .../queries/query_correlation.ts | 8 +- .../query_correlation_with_histogram.test.ts} | 99 ++--- .../query_correlation_with_histogram.ts | 65 +++ .../queries/query_failure_correlation.ts | 9 +- .../queries/query_field_candidates.test.ts | 2 +- .../queries/query_field_candidates.ts | 11 +- .../queries/query_field_value_pairs.test.ts | 17 +- .../queries/query_field_value_pairs.ts | 88 ++++ .../queries/query_fractions.test.ts | 1 + .../queries/query_fractions.ts | 14 +- .../queries/query_histogram.test.ts | 0 .../queries/query_histogram.ts | 8 +- .../query_histogram_range_steps.test.ts | 0 .../queries/query_histogram_range_steps.ts | 6 +- .../correlations/queries/query_p_values.ts | 58 +++ .../queries/query_percentiles.test.ts | 0 .../queries/query_percentiles.ts | 10 +- .../queries/query_ranges.test.ts | 0 .../queries/query_ranges.ts | 8 +- .../queries/query_significant_correlations.ts | 87 ++++ .../compute_expectations_and_ranges.test.ts | 0 .../utils/compute_expectations_and_ranges.ts | 23 +- .../utils/field_stats_utils.ts | 0 .../utils/index.ts | 2 +- .../utils/split_all_settled_promises.ts | 29 ++ .../get_overall_latency_distribution.ts | 8 +- .../latency/get_percentile_threshold_value.ts | 2 +- .../plugins/apm/server/lib/latency/types.ts | 7 +- ...ransactions_correlations_search_service.ts | 259 ------------ ...tions_correlations_search_service_state.ts | 131 ------ .../apm/server/lib/search_strategies/index.ts | 8 - .../latency_correlations/index.ts | 8 - .../latency_correlations_search_service.ts | 293 ------------- ..._correlations_search_service_state.test.ts | 62 --- ...tency_correlations_search_service_state.ts | 121 ------ .../queries/query_field_value_pairs.ts | 124 ------ .../queries/query_histograms_generator.ts | 96 ----- .../register_search_strategies.ts | 40 -- .../search_service_log.test.ts | 47 --- .../search_strategies/search_service_log.ts | 34 -- .../search_strategy_provider.test.ts | 302 ------------- .../search_strategy_provider.ts | 204 --------- x-pack/plugins/apm/server/plugin.ts | 21 - .../plugins/apm/server/routes/correlations.ts | 256 +++++++++++ .../get_global_apm_server_route_repository.ts | 2 + .../correlations/failed_transactions.spec.ts | 351 ++++++++------- .../correlations/field_candidates.spec.ts | 55 +++ .../correlations/field_value_pairs.spec.ts | 71 ++++ .../tests/correlations/latency.spec.ts | 374 ++++++++-------- .../tests/correlations/p_values.spec.ts | 71 ++++ .../significant_correlations.spec.ts | 95 +++++ 97 files changed, 2815 insertions(+), 2760 deletions(-) rename x-pack/plugins/apm/{server/lib/search_strategies => common/correlations}/constants.ts (92%) rename x-pack/plugins/apm/common/{search_strategies => correlations}/failed_transactions_correlations/constants.ts (100%) rename x-pack/plugins/apm/common/{search_strategies => correlations}/failed_transactions_correlations/types.ts (86%) rename x-pack/plugins/apm/common/{search_strategies => correlations}/field_stats_types.ts (90%) rename x-pack/plugins/apm/common/{search_strategies => correlations}/latency_correlations/types.ts (60%) rename x-pack/plugins/apm/common/{search_strategies => correlations}/types.ts (66%) rename x-pack/plugins/apm/{server/lib/search_strategies/queries => common/correlations/utils}/get_prioritized_field_value_pairs.test.ts (100%) rename x-pack/plugins/apm/{server/lib/search_strategies/queries => common/correlations/utils}/get_prioritized_field_value_pairs.ts (88%) rename x-pack/plugins/apm/{server/lib/search_strategies => common/correlations}/utils/has_prefix_to_include.test.ts (100%) rename x-pack/plugins/apm/{server/lib/search_strategies => common/correlations}/utils/has_prefix_to_include.ts (100%) rename x-pack/plugins/apm/{server/lib/search_strategies/failed_transactions_correlations => common/correlations/utils}/index.ts (63%) delete mode 100644 x-pack/plugins/apm/common/search_strategies/constants.ts delete mode 100644 x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx create mode 100644 x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx create mode 100644 x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts create mode 100644 x-pack/plugins/apm/public/components/app/correlations/use_fetch_params.ts create mode 100644 x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx create mode 100644 x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts create mode 100644 x-pack/plugins/apm/public/components/app/correlations/utils/analysis_hook_utils.ts delete mode 100644 x-pack/plugins/apm/public/hooks/use_search_strategy.ts rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/field_stats/get_boolean_field_stats.ts (93%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/field_stats/get_field_stats.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/field_stats/get_fields_stats.ts (94%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/field_stats/get_keyword_field_stats.ts (93%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/field_stats/get_numeric_field_stats.ts (95%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/get_filters.ts (91%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/get_query_with_params.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/get_query_with_params.ts (91%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/get_request_base.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/get_request_base.ts (79%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/index.ts (79%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_correlation.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_correlation.ts (95%) rename x-pack/plugins/apm/server/lib/{search_strategies/queries/query_histograms_generator.test.ts => correlations/queries/query_correlation_with_histogram.test.ts} (55%) create mode 100644 x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.ts rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_failure_correlation.ts (92%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_field_candidates.test.ts (98%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_field_candidates.ts (91%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_field_value_pairs.test.ts (81%) create mode 100644 x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.ts rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_fractions.test.ts (98%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_fractions.ts (87%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_histogram.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_histogram.ts (92%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_histogram_range_steps.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_histogram_range_steps.ts (93%) create mode 100644 x-pack/plugins/apm/server/lib/correlations/queries/query_p_values.ts rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_percentiles.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_percentiles.ts (91%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_ranges.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/queries/query_ranges.ts (93%) create mode 100644 x-pack/plugins/apm/server/lib/correlations/queries/query_significant_correlations.ts rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/utils/compute_expectations_and_ranges.test.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/utils/compute_expectations_and_ranges.ts (79%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/utils/field_stats_utils.ts (100%) rename x-pack/plugins/apm/server/lib/{search_strategies => correlations}/utils/index.ts (82%) create mode 100644 x-pack/plugins/apm/server/lib/correlations/utils/split_all_settled_promises.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations.ts create mode 100644 x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts create mode 100644 x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts create mode 100644 x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts create mode 100644 x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/constants.ts b/x-pack/plugins/apm/common/correlations/constants.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/constants.ts rename to x-pack/plugins/apm/common/correlations/constants.ts index 5af1b21630720..11b9a9a109dbf 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/constants.ts +++ b/x-pack/plugins/apm/common/correlations/constants.ts @@ -82,9 +82,5 @@ export const KS_TEST_THRESHOLD = 0.1; export const ERROR_CORRELATION_THRESHOLD = 0.02; -/** - * Field stats/top values sampling constants - */ - -export const SAMPLER_TOP_TERMS_THRESHOLD = 100000; -export const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000; +export const DEFAULT_PERCENTILE_THRESHOLD = 95; +export const DEBOUNCE_INTERVAL = 100; diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/constants.ts b/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/constants.ts similarity index 100% rename from x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/constants.ts rename to x-pack/plugins/apm/common/correlations/failed_transactions_correlations/constants.ts diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts similarity index 86% rename from x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts rename to x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts index 28ce2ff24b961..8b09d45c1e1b6 100644 --- a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts +++ b/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts @@ -24,12 +24,8 @@ export interface FailedTransactionsCorrelation extends FieldValuePair { export type FailedTransactionsCorrelationsImpactThreshold = typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD]; -export interface FailedTransactionsCorrelationsParams { - percentileThreshold: number; -} - -export interface FailedTransactionsCorrelationsRawResponse { - log: string[]; +export interface FailedTransactionsCorrelationsResponse { + ccsWarning: boolean; failedTransactionsCorrelations?: FailedTransactionsCorrelation[]; percentileThresholdValue?: number; overallHistogram?: HistogramItem[]; diff --git a/x-pack/plugins/apm/common/search_strategies/field_stats_types.ts b/x-pack/plugins/apm/common/correlations/field_stats_types.ts similarity index 90% rename from x-pack/plugins/apm/common/search_strategies/field_stats_types.ts rename to x-pack/plugins/apm/common/correlations/field_stats_types.ts index d63dd7f8d58a1..50dc7919fbd00 100644 --- a/x-pack/plugins/apm/common/search_strategies/field_stats_types.ts +++ b/x-pack/plugins/apm/common/correlations/field_stats_types.ts @@ -6,9 +6,9 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SearchStrategyParams } from './types'; +import { CorrelationsParams } from './types'; -export interface FieldStatsCommonRequestParams extends SearchStrategyParams { +export interface FieldStatsCommonRequestParams extends CorrelationsParams { samplerShardSize: number; } diff --git a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts b/x-pack/plugins/apm/common/correlations/latency_correlations/types.ts similarity index 60% rename from x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts rename to x-pack/plugins/apm/common/correlations/latency_correlations/types.ts index ea74175a3dacb..23c91554b6547 100644 --- a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts +++ b/x-pack/plugins/apm/common/correlations/latency_correlations/types.ts @@ -14,22 +14,8 @@ export interface LatencyCorrelation extends FieldValuePair { ksTest: number; } -export interface LatencyCorrelationSearchServiceProgress { - started: number; - loadedHistogramStepsize: number; - loadedOverallHistogram: number; - loadedFieldCandidates: number; - loadedFieldValuePairs: number; - loadedHistograms: number; -} - -export interface LatencyCorrelationsParams { - percentileThreshold: number; - analyzeCorrelations: boolean; -} - -export interface LatencyCorrelationsRawResponse { - log: string[]; +export interface LatencyCorrelationsResponse { + ccsWarning: boolean; overallHistogram?: HistogramItem[]; percentileThresholdValue?: number; latencyCorrelations?: LatencyCorrelation[]; diff --git a/x-pack/plugins/apm/common/search_strategies/types.ts b/x-pack/plugins/apm/common/correlations/types.ts similarity index 66% rename from x-pack/plugins/apm/common/search_strategies/types.ts rename to x-pack/plugins/apm/common/correlations/types.ts index ff925f70fc9b0..402750b72b2ab 100644 --- a/x-pack/plugins/apm/common/search_strategies/types.ts +++ b/x-pack/plugins/apm/common/correlations/types.ts @@ -26,35 +26,20 @@ export interface ResponseHit { _source: ResponseHitSource; } -export interface RawResponseBase { - ccsWarning: boolean; - took: number; -} - -export interface SearchStrategyClientParamsBase { +export interface CorrelationsClientParams { environment: string; kuery: string; serviceName?: string; transactionName?: string; transactionType?: string; -} - -export interface RawSearchStrategyClientParams - extends SearchStrategyClientParamsBase { - start?: string; - end?: string; -} - -export interface SearchStrategyClientParams - extends SearchStrategyClientParamsBase { start: number; end: number; } -export interface SearchStrategyServerParams { +export interface CorrelationsServerParams { index: string; includeFrozen?: boolean; } -export type SearchStrategyParams = SearchStrategyClientParams & - SearchStrategyServerParams; +export type CorrelationsParams = CorrelationsClientParams & + CorrelationsServerParams; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.test.ts b/x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.test.ts rename to x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts b/x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.ts similarity index 88% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts rename to x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.ts index 6338422b022da..4a0086ba02a6d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts +++ b/x-pack/plugins/apm/common/correlations/utils/get_prioritized_field_value_pairs.ts @@ -6,9 +6,9 @@ */ import { FIELDS_TO_ADD_AS_CANDIDATE } from '../constants'; -import { hasPrefixToInclude } from '../utils'; +import { hasPrefixToInclude } from './has_prefix_to_include'; -import type { FieldValuePair } from '../../../../common/search_strategies/types'; +import type { FieldValuePair } from '../types'; export const getPrioritizedFieldValuePairs = ( fieldValuePairs: FieldValuePair[] diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.test.ts b/x-pack/plugins/apm/common/correlations/utils/has_prefix_to_include.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.test.ts rename to x-pack/plugins/apm/common/correlations/utils/has_prefix_to_include.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.ts b/x-pack/plugins/apm/common/correlations/utils/has_prefix_to_include.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.ts rename to x-pack/plugins/apm/common/correlations/utils/has_prefix_to_include.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/common/correlations/utils/index.ts similarity index 63% rename from x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts rename to x-pack/plugins/apm/common/correlations/utils/index.ts index 4763cd994d309..eb83c8ae2ed01 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts +++ b/x-pack/plugins/apm/common/correlations/utils/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations_search_service'; +export { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; +export { hasPrefixToInclude } from './has_prefix_to_include'; diff --git a/x-pack/plugins/apm/common/search_strategies/constants.ts b/x-pack/plugins/apm/common/search_strategies/constants.ts deleted file mode 100644 index 58203c93e5a42..0000000000000 --- a/x-pack/plugins/apm/common/search_strategies/constants.ts +++ /dev/null @@ -1,15 +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 APM_SEARCH_STRATEGIES = { - APM_FAILED_TRANSACTIONS_CORRELATIONS: 'apmFailedTransactionsCorrelations', - APM_LATENCY_CORRELATIONS: 'apmLatencyCorrelations', -} as const; -export type ApmSearchStrategies = - typeof APM_SEARCH_STRATEGIES[keyof typeof APM_SEARCH_STRATEGIES]; - -export const DEFAULT_PERCENTILE_THRESHOLD = 95; diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx index 4a0f7d81e24dc..7165aa67a5e5a 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx @@ -19,7 +19,7 @@ import { import React, { Fragment, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { FieldStats } from '../../../../../common/search_strategies/field_stats_types'; +import { FieldStats } from '../../../../../common/correlations/field_stats_types'; import { OnAddFilter, TopValues } from './top_values'; import { useTheme } from '../../../../hooks/use_theme'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx index 803b474fe7754..05b4f6d56fa45 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx @@ -14,7 +14,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldStats } from '../../../../../common/search_strategies/field_stats_types'; +import { FieldStats } from '../../../../../common/correlations/field_stats_types'; import { asPercent } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx deleted file mode 100644 index 2115918a71415..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx +++ /dev/null @@ -1,38 +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 { EuiAccordion, EuiCode, EuiPanel } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; - -interface Props { - logMessages: string[]; -} -export function CorrelationsLog({ logMessages }: Props) { - return ( - - - {logMessages.map((logMessage, i) => { - const [timestamp, message] = logMessage.split(': '); - return ( -

- - {asAbsoluteDateTime(timestamp)} {message} - -

- ); - })} -
-
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index eda3b64c309cc..a2026b0a8abea 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -14,7 +14,7 @@ import type { Criteria } from '@elastic/eui/src/components/basic_table/basic_tab import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useUiTracker } from '../../../../../observability/public'; import { useTheme } from '../../../hooks/use_theme'; -import type { FieldValuePair } from '../../../../common/search_strategies/types'; +import type { FieldValuePair } from '../../../../common/correlations/types'; const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50]; diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx index 838671cbae7d9..f13d360444923 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -29,23 +29,16 @@ import type { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - enableInspectEsQueries, - useUiTracker, -} from '../../../../../observability/public'; +import { useUiTracker } from '../../../../../observability/public'; import { asPercent } from '../../../../common/utils/formatters'; -import { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types'; -import { - APM_SEARCH_STRATEGIES, - DEFAULT_PERCENTILE_THRESHOLD, -} from '../../../../common/search_strategies/constants'; -import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; +import { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../common/correlations/constants'; +import { FieldStats } from '../../../../common/correlations/field_stats_types'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useSearchStrategy } from '../../../hooks/use_search_strategy'; import { useTheme } from '../../../hooks/use_theme'; import { ImpactBar } from '../../shared/ImpactBar'; @@ -53,14 +46,12 @@ import { push } from '../../shared/Links/url_helpers'; import { CorrelationsTable } from './correlations_table'; import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; -import { isErrorMessage } from './utils/is_error_message'; import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; import { getOverallHistogram } from './utils/get_overall_histogram'; import { TransactionDistributionChart, TransactionDistributionChartData, } from '../../shared/charts/transaction_distribution_chart'; -import { CorrelationsLog } from './correlations_log'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; @@ -68,6 +59,8 @@ import { useTransactionColors } from './use_transaction_colors'; import { CorrelationsContextPopover } from './context_popover'; import { OnAddFilter } from './context_popover/top_values'; +import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; + export function FailedTransactionsCorrelations({ onFilter, }: { @@ -77,18 +70,12 @@ export function FailedTransactionsCorrelations({ const transactionColors = useTransactionColors(); const { - core: { notifications, uiSettings }, + core: { notifications }, } = useApmPluginContext(); const trackApmEvent = useUiTracker({ app: 'apm' }); - const inspectEnabled = uiSettings.get(enableInspectEsQueries); - - const { progress, response, startFetch, cancelFetch } = useSearchStrategy( - APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, - { - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - } - ); + const { progress, response, startFetch, cancelFetch } = + useFailedTransactionsCorrelations(); const fieldStats: Record | undefined = useMemo(() => { return response.fieldStats?.reduce((obj, field) => { @@ -97,7 +84,6 @@ export function FailedTransactionsCorrelations({ }, {} as Record); }, [response?.fieldStats]); - const progressNormalized = progress.loaded / progress.total; const { overallHistogram, hasData, status } = getOverallHistogram( response, progress.isRunning @@ -368,7 +354,7 @@ export function FailedTransactionsCorrelations({ }, [fieldStats, onAddFilter, showStats]); useEffect(() => { - if (isErrorMessage(progress.error)) { + if (progress.error) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.correlations.failedTransactions.errorTitle', @@ -377,7 +363,7 @@ export function FailedTransactionsCorrelations({ 'An error occurred performing correlations on failed transactions', } ), - text: progress.error.toString(), + text: progress.error, }); } }, [progress.error, notifications.toasts]); @@ -439,7 +425,7 @@ export function FailedTransactionsCorrelations({ const showCorrelationsEmptyStatePrompt = correlationTerms.length < 1 && - (progressNormalized === 1 || !progress.isRunning); + (progress.loaded === 1 || !progress.isRunning); const transactionDistributionChartData: TransactionDistributionChartData[] = []; @@ -457,8 +443,8 @@ export function FailedTransactionsCorrelations({ if (Array.isArray(response.errorHistogram)) { transactionDistributionChartData.push({ id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.allFailedTransactionsLabel', - { defaultMessage: 'All failed transactions' } + 'xpack.apm.transactionDistribution.chart.failedTransactionsLabel', + { defaultMessage: 'Failed transactions' } ), histogram: response.errorHistogram, }); @@ -525,7 +511,7 @@ export function FailedTransactionsCorrelations({ , allTransactions: ( @@ -536,13 +522,13 @@ export function FailedTransactionsCorrelations({ /> ), - allFailedTransactions: ( + failedTransactions: ( ), @@ -621,7 +607,7 @@ export function FailedTransactionsCorrelations({ }
- {inspectEnabled && }
); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx index 918f94e64ef09..b6bd267e746b3 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx @@ -18,8 +18,7 @@ import { dataPluginMock } from 'src/plugins/data/public/mocks'; import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import type { LatencyCorrelationsRawResponse } from '../../../../common/search_strategies/latency_correlations/types'; -import type { RawResponseBase } from '../../../../common/search_strategies/types'; +import type { LatencyCorrelationsResponse } from '../../../../common/correlations/latency_correlations/types'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -35,9 +34,7 @@ function Wrapper({ dataSearchResponse, }: { children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse< - LatencyCorrelationsRawResponse & RawResponseBase - >; + dataSearchResponse: IKibanaSearchResponse; }) { const mockDataSearch = jest.fn(() => of(dataSearchResponse)); @@ -99,9 +96,7 @@ describe('correlations', () => { isRunning: true, rawResponse: { ccsWarning: false, - took: 1234, latencyCorrelations: [], - log: [], }, }} > @@ -122,9 +117,7 @@ describe('correlations', () => { isRunning: false, rawResponse: { ccsWarning: false, - took: 1234, latencyCorrelations: [], - log: [], }, }} > 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 db6f3ad63f00d..b67adc03d40e9 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 @@ -25,22 +25,15 @@ import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/tab import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - enableInspectEsQueries, - useUiTracker, -} from '../../../../../observability/public'; +import { useUiTracker } from '../../../../../observability/public'; import { asPreciseDecimal } from '../../../../common/utils/formatters'; -import { - APM_SEARCH_STRATEGIES, - DEFAULT_PERCENTILE_THRESHOLD, -} from '../../../../common/search_strategies/constants'; -import { LatencyCorrelation } from '../../../../common/search_strategies/latency_correlations/types'; -import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../common/correlations/constants'; +import { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; +import { FieldStats } from '../../../../common/correlations/field_stats_types'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useSearchStrategy } from '../../../hooks/use_search_strategy'; import { TransactionDistributionChart, @@ -50,33 +43,24 @@ import { push } from '../../shared/Links/url_helpers'; import { CorrelationsTable } from './correlations_table'; import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; -import { isErrorMessage } from './utils/is_error_message'; import { getOverallHistogram } from './utils/get_overall_histogram'; -import { CorrelationsLog } from './correlations_log'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; import { useTransactionColors } from './use_transaction_colors'; import { CorrelationsContextPopover } from './context_popover'; import { OnAddFilter } from './context_popover/top_values'; +import { useLatencyCorrelations } from './use_latency_correlations'; export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { const transactionColors = useTransactionColors(); const { - core: { notifications, uiSettings }, + core: { notifications }, } = useApmPluginContext(); - const displayLog = uiSettings.get(enableInspectEsQueries); - - const { progress, response, startFetch, cancelFetch } = useSearchStrategy( - APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, - { - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - analyzeCorrelations: true, - } - ); - const progressNormalized = progress.loaded / progress.total; + const { progress, response, startFetch, cancelFetch } = + useLatencyCorrelations(); const { overallHistogram, hasData, status } = getOverallHistogram( response, progress.isRunning @@ -90,7 +74,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { }, [response?.fieldStats]); useEffect(() => { - if (isErrorMessage(progress.error)) { + if (progress.error) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.correlations.latencyCorrelations.errorTitle', @@ -98,7 +82,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { defaultMessage: 'An error occurred fetching correlations', } ), - text: progress.error.toString(), + text: progress.error, }); } }, [progress.error, notifications.toasts]); @@ -288,8 +272,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { const showCorrelationsTable = progress.isRunning || histogramTerms.length > 0; const showCorrelationsEmptyStatePrompt = - histogramTerms.length < 1 && - (progressNormalized === 1 || !progress.isRunning); + histogramTerms.length < 1 && (progress.loaded === 1 || !progress.isRunning); const transactionDistributionChartData: TransactionDistributionChartData[] = []; @@ -382,7 +365,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { void }) { )} {showCorrelationsEmptyStatePrompt && }
- {displayLog && } ); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx new file mode 100644 index 0000000000000..929cc4f7f4cd3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx @@ -0,0 +1,399 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode } from 'react'; +import { merge } from 'lodash'; +import { createMemoryHistory } from 'history'; +import { renderHook, act } from '@testing-library/react-hooks'; + +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { + mockApmPluginContextValue, + MockApmPluginContextWrapper, +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { delay } from '../../../utils/testHelpers'; + +import { fromQuery } from '../../shared/Links/url_helpers'; + +import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; + +function wrapper({ + children, + error = false, +}: { + children?: ReactNode; + error: boolean; +}) { + const httpMethodMock = jest.fn().mockImplementation(async (endpoint) => { + await delay(100); + if (error) { + throw new Error('Something went wrong'); + } + switch (endpoint) { + case '/internal/apm/latency/overall_distribution': + return { + overallHistogram: [{ key: 'the-key', doc_count: 1234 }], + percentileThresholdValue: 1.234, + }; + case '/internal/apm/correlations/field_candidates': + return { fieldCandidates: ['field-1', 'field2'] }; + case '/internal/apm/correlations/field_value_pairs': + return { + fieldValuePairs: [ + { fieldName: 'field-name-1', fieldValue: 'field-value-1' }, + ], + }; + case '/internal/apm/correlations/p_values': + return { + failedTransactionsCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + doc_count: 123, + bg_count: 1234, + score: 0.66, + pValue: 0.01, + normalizedScore: 0.85, + failurePercentage: 30, + successPercentage: 70, + histogram: [{ key: 'the-key', doc_count: 123 }], + }, + ], + }; + case '/internal/apm/correlations/field_stats': + return { + stats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + }; + default: + return {}; + } + }); + + const history = createMemoryHistory(); + jest.spyOn(history, 'push'); + jest.spyOn(history, 'replace'); + + history.replace({ + pathname: '/services/the-service-name/transactions/view', + search: fromQuery({ + transactionName: 'the-transaction-name', + rangeFrom: 'now-15m', + rangeTo: 'now', + }), + }); + + const mockPluginContext = merge({}, mockApmPluginContextValue, { + core: { http: { get: httpMethodMock, post: httpMethodMock } }, + }) as unknown as ApmPluginContextValue; + + return ( + + {children} + + ); +} + +describe('useFailedTransactionsCorrelations', () => { + beforeEach(async () => { + jest.useFakeTimers(); + }); + // Running all pending timers and switching to real timers using Jest + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + describe('when successfully loading results', () => { + it('should automatically start fetching results', async () => { + const { result, unmount } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + } + ); + + try { + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + expect(typeof result.current.startFetch).toEqual('function'); + expect(typeof result.current.cancelFetch).toEqual('function'); + } finally { + unmount(); + } + }); + + it('should not have received any results after 50ms', async () => { + const { result, unmount } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(50); + + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + } finally { + unmount(); + } + }); + + it('should receive partial updates and finish running', async () => { + const { result, unmount, waitFor } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(50); + await waitFor(() => expect(result.current.progress.loaded).toBe(0)); + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0)); + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.05)); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.05, + }); + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: undefined, + errorHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + failedTransactionsCorrelations: undefined, + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.1)); + + // field candidates are an implementation detail and + // will not be exposed, it will just set loaded to 0.1. + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.1, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(1)); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 1, + }); + + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: undefined, + errorHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + failedTransactionsCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + doc_count: 123, + bg_count: 1234, + score: 0.66, + pValue: 0.01, + normalizedScore: 0.85, + failurePercentage: 30, + successPercentage: 70, + histogram: [{ key: 'the-key', doc_count: 123 }], + }, + ], + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => + expect(result.current.response.fieldStats).toBeDefined() + ); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: false, + loaded: 1, + }); + + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + errorHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + failedTransactionsCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + doc_count: 123, + bg_count: 1234, + score: 0.66, + pValue: 0.01, + normalizedScore: 0.85, + failurePercentage: 30, + successPercentage: 70, + histogram: [{ key: 'the-key', doc_count: 123 }], + }, + ], + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + } finally { + unmount(); + } + }); + }); + describe('when throwing an error', () => { + it('should automatically start fetching results', async () => { + const { result, unmount } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + initialProps: { + error: true, + }, + } + ); + + try { + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + } finally { + unmount(); + } + }); + + it('should still be running after 50ms', async () => { + const { result, unmount } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + initialProps: { + error: true, + }, + } + ); + + try { + jest.advanceTimersByTime(50); + + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + } finally { + unmount(); + } + }); + + it('should stop and return an error after more than 100ms', async () => { + const { result, unmount, waitFor } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + initialProps: { + error: true, + }, + } + ); + + try { + jest.advanceTimersByTime(150); + await waitFor(() => + expect(result.current.progress.error).toBeDefined() + ); + + expect(result.current.progress).toEqual({ + error: 'Something went wrong', + isRunning: false, + loaded: 0, + }); + } finally { + unmount(); + } + }); + }); + + describe('when canceled', () => { + it('should stop running', async () => { + const { result, unmount, waitFor } = renderHook( + () => useFailedTransactionsCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(50); + await waitFor(() => expect(result.current.progress.loaded).toBe(0)); + + expect(result.current.progress.isRunning).toBe(true); + + act(() => { + result.current.cancelFetch(); + }); + + await waitFor(() => + expect(result.current.progress.isRunning).toEqual(false) + ); + } finally { + unmount(); + } + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts new file mode 100644 index 0000000000000..163223e744a22 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts @@ -0,0 +1,257 @@ +/* + * 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 { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; +import { chunk, debounce } from 'lodash'; + +import { IHttpFetchError, ResponseErrorBody } from 'src/core/public'; + +import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { + DEBOUNCE_INTERVAL, + DEFAULT_PERCENTILE_THRESHOLD, +} from '../../../../common/correlations/constants'; +import type { + FailedTransactionsCorrelation, + FailedTransactionsCorrelationsResponse, +} from '../../../../common/correlations/failed_transactions_correlations/types'; + +import { callApmApi } from '../../../services/rest/createCallApmApi'; + +import { + getInitialResponse, + getFailedTransactionsCorrelationsSortedByScore, + getReducer, + CorrelationsProgress, +} from './utils/analysis_hook_utils'; +import { useFetchParams } from './use_fetch_params'; + +// Overall progress is a float from 0 to 1. +const LOADED_OVERALL_HISTOGRAM = 0.05; +const LOADED_FIELD_CANDIDATES = LOADED_OVERALL_HISTOGRAM + 0.05; +const LOADED_DONE = 1; +const PROGRESS_STEP_P_VALUES = 0.9; + +export function useFailedTransactionsCorrelations() { + const fetchParams = useFetchParams(); + + // This use of useReducer (the dispatch function won't get reinstantiated + // on every update) and debounce avoids flooding consuming components with updates. + // `setResponse.flush()` can be used to enforce an update. + const [response, setResponseUnDebounced] = useReducer( + getReducer(), + getInitialResponse() + ); + const setResponse = useMemo( + () => debounce(setResponseUnDebounced, DEBOUNCE_INTERVAL), + [] + ); + + const abortCtrl = useRef(new AbortController()); + + const startFetch = useCallback(async () => { + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + setResponse({ + ...getInitialResponse(), + isRunning: true, + // explicitly set these to undefined to override a possible previous state. + error: undefined, + failedTransactionsCorrelations: undefined, + percentileThresholdValue: undefined, + overallHistogram: undefined, + errorHistogram: undefined, + fieldStats: undefined, + }); + setResponse.flush(); + + try { + // `responseUpdate` will be enriched with additional data with subsequent + // calls to the overall histogram, field candidates, field value pairs, correlation results + // and histogram data for statistically significant results. + const responseUpdate: FailedTransactionsCorrelationsResponse = { + ccsWarning: false, + }; + + const [overallHistogramResponse, errorHistogramRespone] = + await Promise.all([ + // Initial call to fetch the overall distribution for the log-log plot. + callApmApi({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, + }, + }), + callApmApi({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + termFilters: [ + { + fieldName: EVENT_OUTCOME, + fieldValue: EventOutcome.failure, + }, + ], + }, + }, + }), + ]); + + const { overallHistogram, percentileThresholdValue } = + overallHistogramResponse; + const { overallHistogram: errorHistogram } = errorHistogramRespone; + + responseUpdate.errorHistogram = errorHistogram; + responseUpdate.overallHistogram = overallHistogram; + responseUpdate.percentileThresholdValue = percentileThresholdValue; + + if (abortCtrl.current.signal.aborted) { + return; + } + + setResponse({ + ...responseUpdate, + loaded: LOADED_OVERALL_HISTOGRAM, + }); + setResponse.flush(); + + const { fieldCandidates: candidates } = await callApmApi({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + signal: abortCtrl.current.signal, + params: { + query: fetchParams, + }, + }); + + if (abortCtrl.current.signal.aborted) { + return; + } + + const fieldCandidates = candidates.filter((t) => !(t === EVENT_OUTCOME)); + + setResponse({ + loaded: LOADED_FIELD_CANDIDATES, + }); + setResponse.flush(); + + const failedTransactionsCorrelations: FailedTransactionsCorrelation[] = + []; + const fieldsToSample = new Set(); + const chunkSize = 10; + let chunkLoadCounter = 0; + + const fieldCandidatesChunks = chunk(fieldCandidates, chunkSize); + + for (const fieldCandidatesChunk of fieldCandidatesChunks) { + const pValues = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/p_values', + signal: abortCtrl.current.signal, + params: { + body: { ...fetchParams, fieldCandidates: fieldCandidatesChunk }, + }, + }); + + if (pValues.failedTransactionsCorrelations.length > 0) { + pValues.failedTransactionsCorrelations.forEach((d) => { + fieldsToSample.add(d.fieldName); + }); + failedTransactionsCorrelations.push( + ...pValues.failedTransactionsCorrelations + ); + responseUpdate.failedTransactionsCorrelations = + getFailedTransactionsCorrelationsSortedByScore([ + ...failedTransactionsCorrelations, + ]); + } + + chunkLoadCounter++; + setResponse({ + ...responseUpdate, + loaded: + LOADED_FIELD_CANDIDATES + + (chunkLoadCounter / fieldCandidatesChunks.length) * + PROGRESS_STEP_P_VALUES, + }); + + if (abortCtrl.current.signal.aborted) { + return; + } + } + + setResponse.flush(); + + const { stats } = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/field_stats', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + fieldsToSample: [...fieldsToSample], + }, + }, + }); + + responseUpdate.fieldStats = stats; + setResponse({ ...responseUpdate, loaded: LOADED_DONE, isRunning: false }); + setResponse.flush(); + } catch (e) { + if (!abortCtrl.current.signal.aborted) { + const err = e as Error | IHttpFetchError; + setResponse({ + error: + 'response' in err + ? err.body?.message ?? err.response?.statusText + : err.message, + isRunning: false, + }); + setResponse.flush(); + } + } + }, [fetchParams, setResponse]); + + const cancelFetch = useCallback(() => { + abortCtrl.current.abort(); + setResponse({ + isRunning: false, + }); + setResponse.flush(); + }, [setResponse]); + + // auto-update + useEffect(() => { + startFetch(); + return () => { + abortCtrl.current.abort(); + }; + }, [startFetch, cancelFetch]); + + const { error, loaded, isRunning, ...returnedResponse } = response; + const progress = useMemo( + () => ({ + error, + loaded, + isRunning, + }), + [error, loaded, isRunning] + ); + + return { + progress, + response: returnedResponse, + startFetch, + cancelFetch, + }; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_fetch_params.ts b/x-pack/plugins/apm/public/components/app/correlations/use_fetch_params.ts new file mode 100644 index 0000000000000..827604f776c5a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_fetch_params.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; + +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; + +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useTimeRange } from '../../../hooks/use_time_range'; + +export const useFetchParams = () => { + const { serviceName } = useApmServiceContext(); + + const { + query: { + kuery, + environment, + rangeFrom, + rangeTo, + transactionName, + transactionType, + }, + } = useApmParams('/services/{serviceName}/transactions/view'); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + + return useMemo( + () => ({ + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + }), + [ + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + ] + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx new file mode 100644 index 0000000000000..90d976c389c58 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx @@ -0,0 +1,360 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode } from 'react'; +import { merge } from 'lodash'; +import { createMemoryHistory } from 'history'; +import { renderHook, act } from '@testing-library/react-hooks'; + +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { + mockApmPluginContextValue, + MockApmPluginContextWrapper, +} from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { delay } from '../../../utils/testHelpers'; + +import { fromQuery } from '../../shared/Links/url_helpers'; + +import { useLatencyCorrelations } from './use_latency_correlations'; + +function wrapper({ + children, + error = false, +}: { + children?: ReactNode; + error: boolean; +}) { + const httpMethodMock = jest.fn().mockImplementation(async (endpoint) => { + await delay(100); + if (error) { + throw new Error('Something went wrong'); + } + switch (endpoint) { + case '/internal/apm/latency/overall_distribution': + return { + overallHistogram: [{ key: 'the-key', doc_count: 1234 }], + percentileThresholdValue: 1.234, + }; + case '/internal/apm/correlations/field_candidates': + return { fieldCandidates: ['field-1', 'field2'] }; + case '/internal/apm/correlations/field_value_pairs': + return { + fieldValuePairs: [ + { fieldName: 'field-name-1', fieldValue: 'field-value-1' }, + ], + }; + case '/internal/apm/correlations/significant_correlations': + return { + latencyCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + correlation: 0.5, + histogram: [{ key: 'the-key', doc_count: 123 }], + ksTest: 0.001, + }, + ], + }; + case '/internal/apm/correlations/field_stats': + return { + stats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + }; + default: + return {}; + } + }); + + const history = createMemoryHistory(); + jest.spyOn(history, 'push'); + jest.spyOn(history, 'replace'); + + history.replace({ + pathname: '/services/the-service-name/transactions/view', + search: fromQuery({ + transactionName: 'the-transaction-name', + rangeFrom: 'now-15m', + rangeTo: 'now', + }), + }); + + const mockPluginContext = merge({}, mockApmPluginContextValue, { + core: { http: { get: httpMethodMock, post: httpMethodMock } }, + }) as unknown as ApmPluginContextValue; + + return ( + + {children} + + ); +} + +describe('useLatencyCorrelations', () => { + beforeEach(async () => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + + describe('when successfully loading results', () => { + it('should automatically start fetching results', async () => { + const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + wrapper, + }); + + try { + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + expect(typeof result.current.startFetch).toEqual('function'); + expect(typeof result.current.cancelFetch).toEqual('function'); + } finally { + unmount(); + } + }); + + it('should not have received any results after 50ms', async () => { + const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + wrapper, + }); + + try { + jest.advanceTimersByTime(50); + + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + } finally { + unmount(); + } + }); + + it('should receive partial updates and finish running', async () => { + const { result, unmount, waitFor } = renderHook( + () => useLatencyCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(150); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.05)); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.05, + }); + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: undefined, + latencyCorrelations: undefined, + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.1)); + + // field candidates are an implementation detail and + // will not be exposed, it will just set loaded to 0.1. + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.1, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.4)); + + // field value pairs are an implementation detail and + // will not be exposed, it will just set loaded to 0.4. + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 0.4, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => expect(result.current.progress.loaded).toBe(1)); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: true, + loaded: 1, + }); + + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: undefined, + latencyCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + correlation: 0.5, + histogram: [{ key: 'the-key', doc_count: 123 }], + ksTest: 0.001, + }, + ], + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + + jest.advanceTimersByTime(100); + await waitFor(() => + expect(result.current.response.fieldStats).toBeDefined() + ); + + expect(result.current.progress).toEqual({ + error: undefined, + isRunning: false, + loaded: 1, + }); + + expect(result.current.response).toEqual({ + ccsWarning: false, + fieldStats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + latencyCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + correlation: 0.5, + histogram: [{ key: 'the-key', doc_count: 123 }], + ksTest: 0.001, + }, + ], + overallHistogram: [ + { + doc_count: 1234, + key: 'the-key', + }, + ], + percentileThresholdValue: 1.234, + }); + } finally { + unmount(); + } + }); + }); + + describe('when throwing an error', () => { + it('should automatically start fetching results', async () => { + const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + wrapper, + initialProps: { + error: true, + }, + }); + + try { + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + } finally { + unmount(); + } + }); + + it('should still be running after 50ms', async () => { + const { result, unmount } = renderHook(() => useLatencyCorrelations(), { + wrapper, + initialProps: { + error: true, + }, + }); + + try { + jest.advanceTimersByTime(50); + + expect(result.current.progress).toEqual({ + isRunning: true, + loaded: 0, + }); + expect(result.current.response).toEqual({ ccsWarning: false }); + } finally { + unmount(); + } + }); + + it('should stop and return an error after more than 100ms', async () => { + const { result, unmount, waitFor } = renderHook( + () => useLatencyCorrelations(), + { + wrapper, + initialProps: { + error: true, + }, + } + ); + + try { + jest.advanceTimersByTime(150); + await waitFor(() => + expect(result.current.progress.error).toBeDefined() + ); + + expect(result.current.progress).toEqual({ + error: 'Something went wrong', + isRunning: false, + loaded: 0, + }); + } finally { + unmount(); + } + }); + }); + + describe('when canceled', () => { + it('should stop running', async () => { + const { result, unmount, waitFor } = renderHook( + () => useLatencyCorrelations(), + { + wrapper, + } + ); + + try { + jest.advanceTimersByTime(150); + await waitFor(() => expect(result.current.progress.loaded).toBe(0.05)); + + expect(result.current.progress.isRunning).toBe(true); + + act(() => { + result.current.cancelFetch(); + }); + + await waitFor(() => + expect(result.current.progress.isRunning).toEqual(false) + ); + } finally { + unmount(); + } + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts new file mode 100644 index 0000000000000..358d436f8f0a5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; +import { chunk, debounce } from 'lodash'; + +import { IHttpFetchError, ResponseErrorBody } from 'src/core/public'; + +import { + DEBOUNCE_INTERVAL, + DEFAULT_PERCENTILE_THRESHOLD, +} from '../../../../common/correlations/constants'; +import type { FieldValuePair } from '../../../../common/correlations/types'; +import { getPrioritizedFieldValuePairs } from '../../../../common/correlations/utils'; +import type { + LatencyCorrelation, + LatencyCorrelationsResponse, +} from '../../../../common/correlations/latency_correlations/types'; + +import { callApmApi } from '../../../services/rest/createCallApmApi'; + +import { + getInitialResponse, + getLatencyCorrelationsSortedByCorrelation, + getReducer, + CorrelationsProgress, +} from './utils/analysis_hook_utils'; +import { useFetchParams } from './use_fetch_params'; + +// Overall progress is a float from 0 to 1. +const LOADED_OVERALL_HISTOGRAM = 0.05; +const LOADED_FIELD_CANDIDATES = LOADED_OVERALL_HISTOGRAM + 0.05; +const LOADED_FIELD_VALUE_PAIRS = LOADED_FIELD_CANDIDATES + 0.3; +const LOADED_DONE = 1; +const PROGRESS_STEP_FIELD_VALUE_PAIRS = 0.3; +const PROGRESS_STEP_CORRELATIONS = 0.6; + +export function useLatencyCorrelations() { + const fetchParams = useFetchParams(); + + // This use of useReducer (the dispatch function won't get reinstantiated + // on every update) and debounce avoids flooding consuming components with updates. + // `setResponse.flush()` can be used to enforce an update. + const [response, setResponseUnDebounced] = useReducer( + getReducer(), + getInitialResponse() + ); + const setResponse = useMemo( + () => debounce(setResponseUnDebounced, DEBOUNCE_INTERVAL), + [] + ); + + const abortCtrl = useRef(new AbortController()); + + const startFetch = useCallback(async () => { + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + setResponse({ + ...getInitialResponse(), + isRunning: true, + // explicitly set these to undefined to override a possible previous state. + error: undefined, + latencyCorrelations: undefined, + percentileThresholdValue: undefined, + overallHistogram: undefined, + fieldStats: undefined, + }); + setResponse.flush(); + + try { + // `responseUpdate` will be enriched with additional data with subsequent + // calls to the overall histogram, field candidates, field value pairs, correlation results + // and histogram data for statistically significant results. + const responseUpdate: LatencyCorrelationsResponse = { + ccsWarning: false, + }; + + // Initial call to fetch the overall distribution for the log-log plot. + const { overallHistogram, percentileThresholdValue } = await callApmApi({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, + }, + }); + responseUpdate.overallHistogram = overallHistogram; + responseUpdate.percentileThresholdValue = percentileThresholdValue; + + if (abortCtrl.current.signal.aborted) { + return; + } + + setResponse({ + ...responseUpdate, + loaded: LOADED_OVERALL_HISTOGRAM, + }); + setResponse.flush(); + + const { fieldCandidates } = await callApmApi({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + signal: abortCtrl.current.signal, + params: { + query: fetchParams, + }, + }); + + if (abortCtrl.current.signal.aborted) { + return; + } + + setResponse({ + loaded: LOADED_FIELD_CANDIDATES, + }); + setResponse.flush(); + + const chunkSize = 10; + let chunkLoadCounter = 0; + + const fieldValuePairs: FieldValuePair[] = []; + const fieldCandidateChunks = chunk(fieldCandidates, chunkSize); + + for (const fieldCandidateChunk of fieldCandidateChunks) { + const fieldValuePairChunkResponse = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/field_value_pairs', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + fieldCandidates: fieldCandidateChunk, + }, + }, + }); + + if (fieldValuePairChunkResponse.fieldValuePairs.length > 0) { + fieldValuePairs.push(...fieldValuePairChunkResponse.fieldValuePairs); + } + + if (abortCtrl.current.signal.aborted) { + return; + } + + chunkLoadCounter++; + setResponse({ + loaded: + LOADED_FIELD_CANDIDATES + + (chunkLoadCounter / fieldCandidateChunks.length) * + PROGRESS_STEP_FIELD_VALUE_PAIRS, + }); + } + + if (abortCtrl.current.signal.aborted) { + return; + } + + setResponse.flush(); + + chunkLoadCounter = 0; + + const fieldsToSample = new Set(); + const latencyCorrelations: LatencyCorrelation[] = []; + const fieldValuePairChunks = chunk( + getPrioritizedFieldValuePairs(fieldValuePairs), + chunkSize + ); + + for (const fieldValuePairChunk of fieldValuePairChunks) { + const significantCorrelations = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/significant_correlations', + signal: abortCtrl.current.signal, + params: { + body: { ...fetchParams, fieldValuePairs: fieldValuePairChunk }, + }, + }); + + if (significantCorrelations.latencyCorrelations.length > 0) { + significantCorrelations.latencyCorrelations.forEach((d) => { + fieldsToSample.add(d.fieldName); + }); + latencyCorrelations.push( + ...significantCorrelations.latencyCorrelations + ); + responseUpdate.latencyCorrelations = + getLatencyCorrelationsSortedByCorrelation([...latencyCorrelations]); + } + + chunkLoadCounter++; + setResponse({ + ...responseUpdate, + loaded: + LOADED_FIELD_VALUE_PAIRS + + (chunkLoadCounter / fieldValuePairChunks.length) * + PROGRESS_STEP_CORRELATIONS, + }); + + if (abortCtrl.current.signal.aborted) { + return; + } + } + + setResponse.flush(); + + const { stats } = await callApmApi({ + endpoint: 'POST /internal/apm/correlations/field_stats', + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + fieldsToSample: [...fieldsToSample], + }, + }, + }); + + responseUpdate.fieldStats = stats; + setResponse({ + ...responseUpdate, + loaded: LOADED_DONE, + isRunning: false, + }); + setResponse.flush(); + } catch (e) { + if (!abortCtrl.current.signal.aborted) { + const err = e as Error | IHttpFetchError; + setResponse({ + error: + 'response' in err + ? err.body?.message ?? err.response?.statusText + : err.message, + isRunning: false, + }); + setResponse.flush(); + } + } + }, [fetchParams, setResponse]); + + const cancelFetch = useCallback(() => { + abortCtrl.current.abort(); + setResponse({ + isRunning: false, + }); + setResponse.flush(); + }, [setResponse]); + + // auto-update + useEffect(() => { + startFetch(); + return () => { + abortCtrl.current.abort(); + }; + }, [startFetch, cancelFetch]); + + const { error, loaded, isRunning, ...returnedResponse } = response; + const progress = useMemo( + () => ({ + error, + loaded: Math.round(loaded * 100) / 100, + isRunning, + }), + [error, loaded, isRunning] + ); + + return { + progress, + response: returnedResponse, + startFetch, + cancelFetch, + }; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/analysis_hook_utils.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/analysis_hook_utils.ts new file mode 100644 index 0000000000000..24cd76846fa9f --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/analysis_hook_utils.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FailedTransactionsCorrelation } from '../../../../../common/correlations/failed_transactions_correlations/types'; +import type { LatencyCorrelation } from '../../../../../common/correlations/latency_correlations/types'; + +export interface CorrelationsProgress { + error?: string; + isRunning: boolean; + loaded: number; +} + +export function getLatencyCorrelationsSortedByCorrelation( + latencyCorrelations: LatencyCorrelation[] +) { + return latencyCorrelations.sort((a, b) => b.correlation - a.correlation); +} + +export function getFailedTransactionsCorrelationsSortedByScore( + failedTransactionsCorrelations: FailedTransactionsCorrelation[] +) { + return failedTransactionsCorrelations.sort((a, b) => b.score - a.score); +} + +export const getInitialResponse = () => ({ + ccsWarning: false, + isRunning: false, + loaded: 0, +}); + +export const getReducer = + () => + (prev: T, update: Partial): T => ({ + ...prev, + ...update, + }); diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts index e4c08b42b2420..d35833295703f 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts @@ -6,7 +6,7 @@ */ import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label'; -import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failed_transactions_correlations/constants'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/correlations/failed_transactions_correlations/constants'; const EXPECTED_RESULT = { HIGH: { diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts index cbfaee88ff6f4..d5d0fd4dcae51 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts @@ -8,8 +8,8 @@ import { FailedTransactionsCorrelation, FailedTransactionsCorrelationsImpactThreshold, -} from '../../../../../common/search_strategies/failed_transactions_correlations/types'; -import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failed_transactions_correlations/constants'; +} from '../../../../../common/correlations/failed_transactions_correlations/types'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/correlations/failed_transactions_correlations/constants'; export function getFailedTransactionsCorrelationImpactLabel( pValue: FailedTransactionsCorrelation['pValue'] diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts index c323b69594013..b76777b660d8f 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts @@ -5,14 +5,14 @@ * 2.0. */ -import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; +import type { LatencyCorrelationsResponse } from '../../../../../common/correlations/latency_correlations/types'; import { getOverallHistogram } from './get_overall_histogram'; describe('getOverallHistogram', () => { it('returns "loading" when undefined and running', () => { const { overallHistogram, hasData, status } = getOverallHistogram( - {} as LatencyCorrelationsRawResponse, + {} as LatencyCorrelationsResponse, true ); expect(overallHistogram).toStrictEqual(undefined); @@ -22,7 +22,7 @@ describe('getOverallHistogram', () => { it('returns "success" when undefined and not running', () => { const { overallHistogram, hasData, status } = getOverallHistogram( - {} as LatencyCorrelationsRawResponse, + {} as LatencyCorrelationsResponse, false ); expect(overallHistogram).toStrictEqual([]); @@ -34,7 +34,7 @@ describe('getOverallHistogram', () => { const { overallHistogram, hasData, status } = getOverallHistogram( { overallHistogram: [{ key: 1, doc_count: 1234 }], - } as LatencyCorrelationsRawResponse, + } as LatencyCorrelationsResponse, true ); expect(overallHistogram).toStrictEqual([{ key: 1, doc_count: 1234 }]); @@ -46,7 +46,7 @@ describe('getOverallHistogram', () => { const { overallHistogram, hasData, status } = getOverallHistogram( { overallHistogram: [{ key: 1, doc_count: 1234 }], - } as LatencyCorrelationsRawResponse, + } as LatencyCorrelationsResponse, false ); expect(overallHistogram).toStrictEqual([{ key: 1, doc_count: 1234 }]); diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts index 3a90eb4b89123..3a6a2704b3984 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; +import type { LatencyCorrelationsResponse } from '../../../../../common/correlations/latency_correlations/types'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; @@ -13,7 +13,7 @@ import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; // of fetching more data such as correlation results. That's why we have to determine // the `status` of the data for the latency chart separately. export function getOverallHistogram( - data: LatencyCorrelationsRawResponse, + data: LatencyCorrelationsResponse, isRunning: boolean ) { const overallHistogram = diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index ad52adfa13a52..ee2f8fb50a0e5 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { useUiTracker } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/search_strategies/constants'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/correlations/constants'; import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; @@ -165,7 +165,7 @@ export function TransactionDistribution({ @@ -175,13 +175,13 @@ export function TransactionDistribution({ /> ), - allFailedTransactions: ( + failedTransactions: ( ), diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts index 9fb945100414f..a02fc7fe6665f 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts @@ -5,77 +5,41 @@ * 2.0. */ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/search_strategies/constants'; -import { RawSearchStrategyClientParams } from '../../../../../common/search_strategies/types'; +import { DEFAULT_PERCENTILE_THRESHOLD } from '../../../../../common/correlations/constants'; import { EVENT_OUTCOME } from '../../../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../../../common/event_outcome'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; -import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { useApmParams } from '../../../../hooks/use_apm_params'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { useTimeRange } from '../../../../hooks/use_time_range'; import type { TransactionDistributionChartData } from '../../../shared/charts/transaction_distribution_chart'; import { isErrorMessage } from '../../correlations/utils/is_error_message'; - -function hasRequiredParams(params: RawSearchStrategyClientParams) { - const { serviceName, environment, start, end } = params; - return serviceName && environment && start && end; -} +import { useFetchParams } from '../../correlations/use_fetch_params'; export const useTransactionDistributionChartData = () => { - const { serviceName, transactionType } = useApmServiceContext(); + const params = useFetchParams(); const { core: { notifications }, } = useApmPluginContext(); - const { urlParams } = useLegacyUrlParams(); - const { transactionName } = urlParams; - - const { - query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/services/{serviceName}/transactions/view'); - - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - - const params = useMemo( - () => ({ - serviceName, - transactionName, - transactionType, - kuery, - environment, - start, - end, - }), - [ - serviceName, - transactionName, - transactionType, - kuery, - environment, - start, - end, - ] - ); - const { - // TODO The default object has `log: []` to retain compatibility with the shared search strategies code. - // Remove once the other tabs are migrated away from search strategies. - data: overallLatencyData = { log: [] }, + data: overallLatencyData = {}, status: overallLatencyStatus, error: overallLatencyError, } = useFetcher( (callApmApi) => { - if (hasRequiredParams(params)) { + if ( + params.serviceName && + params.environment && + params.start && + params.end + ) { return callApmApi({ endpoint: 'POST /internal/apm/latency/overall_distribution', params: { @@ -114,12 +78,15 @@ export const useTransactionDistributionChartData = () => { Array.isArray(overallLatencyHistogram) && overallLatencyHistogram.length > 0; - // TODO The default object has `log: []` to retain compatibility with the shared search strategies code. - // Remove once the other tabs are migrated away from search strategies. - const { data: errorHistogramData = { log: [] }, error: errorHistogramError } = + const { data: errorHistogramData = {}, error: errorHistogramError } = useFetcher( (callApmApi) => { - if (hasRequiredParams(params)) { + if ( + params.serviceName && + params.environment && + params.start && + params.end + ) { return callApmApi({ endpoint: 'POST /internal/apm/latency/overall_distribution', params: { @@ -171,8 +138,8 @@ export const useTransactionDistributionChartData = () => { if (Array.isArray(errorHistogramData.overallHistogram)) { transactionDistributionChartData.push({ id: i18n.translate( - 'xpack.apm.transactionDistribution.chart.allFailedTransactionsLabel', - { defaultMessage: 'All failed transactions' } + 'xpack.apm.transactionDistribution.chart.failedTransactionsLabel', + { defaultMessage: 'Failed transactions' } ), histogram: errorHistogramData.overallHistogram, }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx index 8a57063ac4d45..b8d070c64ca9f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { HistogramItem } from '../../../../../common/search_strategies/types'; +import type { HistogramItem } from '../../../../../common/correlations/types'; import { replaceHistogramDotsWithBars } from './index'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index dcf52cebaeeda..80fbd864fd815 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -32,7 +32,7 @@ import { i18n } from '@kbn/i18n'; import { useChartTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import type { HistogramItem } from '../../../../../common/search_strategies/types'; +import type { HistogramItem } from '../../../../../common/correlations/types'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; diff --git a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts deleted file mode 100644 index 95bc8cb7435a2..0000000000000 --- a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts +++ /dev/null @@ -1,218 +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 { useCallback, useEffect, useReducer, useRef } from 'react'; -import type { Subscription } from 'rxjs'; - -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - isCompleteResponse, - isErrorResponse, -} from '../../../../../src/plugins/data/public'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; - -import type { RawSearchStrategyClientParams } from '../../common/search_strategies/types'; -import type { RawResponseBase } from '../../common/search_strategies/types'; -import type { - LatencyCorrelationsParams, - LatencyCorrelationsRawResponse, -} from '../../common/search_strategies/latency_correlations/types'; -import type { - FailedTransactionsCorrelationsParams, - FailedTransactionsCorrelationsRawResponse, -} from '../../common/search_strategies/failed_transactions_correlations/types'; -import { - ApmSearchStrategies, - APM_SEARCH_STRATEGIES, -} from '../../common/search_strategies/constants'; -import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; -import { useLegacyUrlParams } from '../context/url_params_context/use_url_params'; - -import { ApmPluginStartDeps } from '../plugin'; - -import { useApmParams } from './use_apm_params'; -import { useTimeRange } from './use_time_range'; - -interface SearchStrategyProgress { - error?: Error; - isRunning: boolean; - loaded: number; - total: number; -} - -const getInitialRawResponse = < - TRawResponse extends RawResponseBase ->(): TRawResponse => - ({ - ccsWarning: false, - took: 0, - } as TRawResponse); - -const getInitialProgress = (): SearchStrategyProgress => ({ - isRunning: false, - loaded: 0, - total: 100, -}); - -const getReducer = - () => - (prev: T, update: Partial): T => ({ - ...prev, - ...update, - }); - -interface SearchStrategyReturnBase { - progress: SearchStrategyProgress; - response: TRawResponse; - startFetch: () => void; - cancelFetch: () => void; -} - -// Function overload for Latency Correlations -export function useSearchStrategy( - searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, - searchStrategyParams: LatencyCorrelationsParams -): SearchStrategyReturnBase; - -// Function overload for Failed Transactions Correlations -export function useSearchStrategy( - searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, - searchStrategyParams: FailedTransactionsCorrelationsParams -): SearchStrategyReturnBase< - FailedTransactionsCorrelationsRawResponse & RawResponseBase ->; - -export function useSearchStrategy< - TRawResponse extends RawResponseBase, - TParams = unknown ->( - searchStrategyName: ApmSearchStrategies, - searchStrategyParams?: TParams -): SearchStrategyReturnBase { - const { - services: { data }, - } = useKibana(); - - const { serviceName, transactionType } = useApmServiceContext(); - const { - query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/services/{serviceName}/transactions/view'); - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { urlParams } = useLegacyUrlParams(); - const { transactionName } = urlParams; - - const [rawResponse, setRawResponse] = useReducer( - getReducer(), - getInitialRawResponse() - ); - - const [fetchState, setFetchState] = useReducer( - getReducer(), - getInitialProgress() - ); - - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(); - const searchStrategyParamsRef = useRef(searchStrategyParams); - - const startFetch = useCallback(() => { - searchSubscription$.current?.unsubscribe(); - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - setFetchState({ - ...getInitialProgress(), - error: undefined, - }); - - const request = { - params: { - environment, - serviceName, - transactionName, - transactionType, - kuery, - start, - end, - ...(searchStrategyParamsRef.current - ? { ...searchStrategyParamsRef.current } - : {}), - }, - }; - - // Submit the search request using the `data.search` service. - searchSubscription$.current = data.search - .search< - IKibanaSearchRequest, - IKibanaSearchResponse - >(request, { - strategy: searchStrategyName, - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (response: IKibanaSearchResponse) => { - setRawResponse(response.rawResponse); - setFetchState({ - isRunning: response.isRunning || false, - ...(response.loaded ? { loaded: response.loaded } : {}), - ...(response.total ? { total: response.total } : {}), - }); - - if (isCompleteResponse(response)) { - searchSubscription$.current?.unsubscribe(); - setFetchState({ - isRunning: false, - }); - } else if (isErrorResponse(response)) { - searchSubscription$.current?.unsubscribe(); - setFetchState({ - error: response as unknown as Error, - isRunning: false, - }); - } - }, - error: (error: Error) => { - setFetchState({ - error, - isRunning: false, - }); - }, - }); - }, [ - searchStrategyName, - data.search, - environment, - serviceName, - transactionName, - transactionType, - kuery, - start, - end, - ]); - - const cancelFetch = useCallback(() => { - searchSubscription$.current?.unsubscribe(); - searchSubscription$.current = undefined; - abortCtrl.current.abort(); - setFetchState({ - isRunning: false, - }); - }, []); - - // auto-update - useEffect(() => { - startFetch(); - return cancelFetch; - }, [startFetch, cancelFetch]); - - return { - progress: fetchState, - response: rawResponse, - startFetch, - cancelFetch, - }; -} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_boolean_field_stats.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_boolean_field_stats.ts index da5493376426c..c936e626a5599 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_boolean_field_stats.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_boolean_field_stats.ts @@ -9,13 +9,13 @@ import { ElasticsearchClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { buildSamplerAggregation } from '../../utils/field_stats_utils'; -import { FieldValuePair } from '../../../../../common/search_strategies/types'; +import { FieldValuePair } from '../../../../../common/correlations/types'; import { FieldStatsCommonRequestParams, BooleanFieldStats, Aggs, TopValueBucket, -} from '../../../../../common/search_strategies/field_stats_types'; +} from '../../../../../common/correlations/field_stats_types'; import { getQueryWithParams } from '../get_query_with_params'; export const getBooleanFieldStatsRequest = ( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_field_stats.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_field_stats.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_fields_stats.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_fields_stats.ts index 2e1441ccbd6a1..8b41f7662679c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_fields_stats.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_fields_stats.ts @@ -10,20 +10,20 @@ import { chunk } from 'lodash'; import { ES_FIELD_TYPES } from '@kbn/field-types'; import { FieldValuePair, - SearchStrategyParams, -} from '../../../../../common/search_strategies/types'; -import { getRequestBase } from '../get_request_base'; -import { fetchKeywordFieldStats } from './get_keyword_field_stats'; -import { fetchNumericFieldStats } from './get_numeric_field_stats'; + CorrelationsParams, +} from '../../../../../common/correlations/types'; import { FieldStats, FieldStatsCommonRequestParams, -} from '../../../../../common/search_strategies/field_stats_types'; +} from '../../../../../common/correlations/field_stats_types'; +import { getRequestBase } from '../get_request_base'; +import { fetchKeywordFieldStats } from './get_keyword_field_stats'; +import { fetchNumericFieldStats } from './get_numeric_field_stats'; import { fetchBooleanFieldStats } from './get_boolean_field_stats'; export const fetchFieldsStats = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, fieldsToSample: string[], termFilters?: FieldValuePair[] ): Promise<{ stats: FieldStats[]; errors: any[] }> => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_keyword_field_stats.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_keyword_field_stats.ts index a9c727457d0ae..c64bbc6678779 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_keyword_field_stats.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_keyword_field_stats.ts @@ -7,15 +7,15 @@ import { ElasticsearchClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { FieldValuePair } from '../../../../../common/search_strategies/types'; -import { getQueryWithParams } from '../get_query_with_params'; -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; +import { FieldValuePair } from '../../../../../common/correlations/types'; import { FieldStatsCommonRequestParams, KeywordFieldStats, Aggs, TopValueBucket, -} from '../../../../../common/search_strategies/field_stats_types'; +} from '../../../../../common/correlations/field_stats_types'; +import { buildSamplerAggregation } from '../../utils/field_stats_utils'; +import { getQueryWithParams } from '../get_query_with_params'; export const getKeywordFieldStatsRequest = ( params: FieldStatsCommonRequestParams, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_numeric_field_stats.ts similarity index 95% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_numeric_field_stats.ts index c45d4356cfe23..21e6559fdda25 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_numeric_field_stats.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/field_stats/get_numeric_field_stats.ts @@ -13,8 +13,8 @@ import { FieldStatsCommonRequestParams, TopValueBucket, Aggs, -} from '../../../../../common/search_strategies/field_stats_types'; -import { FieldValuePair } from '../../../../../common/search_strategies/types'; +} from '../../../../../common/correlations/field_stats_types'; +import { FieldValuePair } from '../../../../../common/correlations/types'; import { getQueryWithParams } from '../get_query_with_params'; import { buildSamplerAggregation } from '../../utils/field_stats_utils'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_filters.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_filters.ts index 4c91f2ca987b5..58ee5051d8863 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_filters.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/get_filters.ts @@ -15,7 +15,7 @@ import { PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { SearchStrategyClientParams } from '../../../../common/search_strategies/types'; +import { CorrelationsClientParams } from '../../../../common/correlations/types'; export function getCorrelationsFilters({ environment, @@ -25,7 +25,7 @@ export function getCorrelationsFilters({ transactionName, start, end, -}: SearchStrategyClientParams) { +}: CorrelationsClientParams) { const correlationsFilters: ESFilter[] = [ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, ...rangeQuery(start, end), diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.ts index 297fd68a7503f..6572d72f614c7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/get_query_with_params.ts @@ -8,8 +8,8 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { FieldValuePair, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getCorrelationsFilters } from './get_filters'; export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => { @@ -17,7 +17,7 @@ export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => { }; interface QueryParams { - params: SearchStrategyParams; + params: CorrelationsParams; termFilters?: FieldValuePair[]; } export const getQueryWithParams = ({ params, termFilters }: QueryParams) => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts similarity index 79% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts index fb1639b5d5f4a..5ab4e3b26122d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/get_request_base.ts @@ -5,12 +5,12 @@ * 2.0. */ -import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import type { CorrelationsParams } from '../../../../common/correlations/types'; export const getRequestBase = ({ index, includeFrozen, -}: SearchStrategyParams) => ({ +}: CorrelationsParams) => ({ index, // matches APM's event client settings ignore_throttled: includeFrozen === undefined ? true : !includeFrozen, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts b/x-pack/plugins/apm/server/lib/correlations/queries/index.ts similarity index 79% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/index.ts index e691b81e4adcf..548127eb7647d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/index.ts @@ -6,11 +6,13 @@ */ export { fetchFailedTransactionsCorrelationPValues } from './query_failure_correlation'; +export { fetchPValues } from './query_p_values'; +export { fetchSignificantCorrelations } from './query_significant_correlations'; export { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; export { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; export { fetchTransactionDurationFractions } from './query_fractions'; export { fetchTransactionDurationPercentiles } from './query_percentiles'; export { fetchTransactionDurationCorrelation } from './query_correlation'; -export { fetchTransactionDurationHistograms } from './query_histograms_generator'; +export { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram'; export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; export { fetchTransactionDurationRanges } from './query_ranges'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.ts similarity index 95% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.ts index a150d23b27113..ed62b4dfa91b7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation.ts @@ -13,8 +13,8 @@ import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldname import type { FieldValuePair, ResponseHit, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; @@ -33,7 +33,7 @@ export interface BucketCorrelation { } export const getTransactionDurationCorrelationRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], @@ -87,7 +87,7 @@ export const getTransactionDurationCorrelationRequest = ( export const fetchTransactionDurationCorrelation = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.test.ts similarity index 55% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.test.ts index 27fd0dc31432d..2e1a635671794 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.test.ts @@ -10,10 +10,9 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { searchServiceLogProvider } from '../search_service_log'; -import { latencyCorrelationsSearchServiceStateProvider } from '../latency_correlations/latency_correlations_search_service_state'; +import { splitAllSettledPromises } from '../utils'; -import { fetchTransactionDurationHistograms } from './query_histograms_generator'; +import { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram'; const params = { index: 'apm-*', @@ -35,8 +34,8 @@ const fieldValuePairs = [ { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-3' }, ]; -describe('query_histograms_generator', () => { - describe('fetchTransactionDurationHistograms', () => { +describe('query_correlation_with_histogram', () => { + describe('fetchTransactionDurationCorrelationWithHistogram', () => { it(`doesn't break on failing ES queries and adds messages to the log`, async () => { const esClientSearchMock = jest.fn( ( @@ -54,37 +53,29 @@ describe('query_histograms_generator', () => { search: esClientSearchMock, } as unknown as ElasticsearchClient; - const state = latencyCorrelationsSearchServiceStateProvider(); - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - let loadedHistograms = 0; - const items = []; - - for await (const item of fetchTransactionDurationHistograms( - esClientMock, - addLogMessage, - params, - state, - expectations, - ranges, - fractions, - histogramRangeSteps, - totalDocCount, - fieldValuePairs - )) { - if (item !== undefined) { - items.push(item); - } - loadedHistograms++; - } + const { fulfilled: items, rejected: errors } = splitAllSettledPromises( + await Promise.allSettled( + fieldValuePairs.map((fieldValuePair) => + fetchTransactionDurationCorrelationWithHistogram( + esClientMock, + params, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePair + ) + ) + ) + ); expect(items.length).toEqual(0); - expect(loadedHistograms).toEqual(3); expect(esClientSearchMock).toHaveBeenCalledTimes(3); - expect(getLogMessages().map((d) => d.split(': ')[1])).toEqual([ - "Failed to fetch correlation/kstest for 'the-field-name-1/the-field-value-1'", - "Failed to fetch correlation/kstest for 'the-field-name-2/the-field-value-2'", - "Failed to fetch correlation/kstest for 'the-field-name-2/the-field-value-3'", + expect(errors.map((e) => (e as Error).toString())).toEqual([ + 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', + 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', + 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', ]); }); @@ -112,34 +103,26 @@ describe('query_histograms_generator', () => { search: esClientSearchMock, } as unknown as ElasticsearchClient; - const state = latencyCorrelationsSearchServiceStateProvider(); - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - let loadedHistograms = 0; - const items = []; - - for await (const item of fetchTransactionDurationHistograms( - esClientMock, - addLogMessage, - params, - state, - expectations, - ranges, - fractions, - histogramRangeSteps, - totalDocCount, - fieldValuePairs - )) { - if (item !== undefined) { - items.push(item); - } - loadedHistograms++; - } + const { fulfilled: items, rejected: errors } = splitAllSettledPromises( + await Promise.allSettled( + fieldValuePairs.map((fieldValuePair) => + fetchTransactionDurationCorrelationWithHistogram( + esClientMock, + params, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePair + ) + ) + ) + ); expect(items.length).toEqual(3); - expect(loadedHistograms).toEqual(3); expect(esClientSearchMock).toHaveBeenCalledTimes(6); - expect(getLogMessages().length).toEqual(0); + expect(errors.length).toEqual(0); }); }); }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.ts new file mode 100644 index 0000000000000..03b28b28d521a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_correlation_with_histogram.ts @@ -0,0 +1,65 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { + FieldValuePair, + CorrelationsParams, +} from '../../../../common/correlations/types'; + +import type { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; +import { + CORRELATION_THRESHOLD, + KS_TEST_THRESHOLD, +} from '../../../../common/correlations/constants'; + +import { fetchTransactionDurationCorrelation } from './query_correlation'; +import { fetchTransactionDurationRanges } from './query_ranges'; + +export async function fetchTransactionDurationCorrelationWithHistogram( + esClient: ElasticsearchClient, + params: CorrelationsParams, + expectations: number[], + ranges: estypes.AggregationsAggregationRange[], + fractions: number[], + histogramRangeSteps: number[], + totalDocCount: number, + fieldValuePair: FieldValuePair +): Promise { + const { correlation, ksTest } = await fetchTransactionDurationCorrelation( + esClient, + params, + expectations, + ranges, + fractions, + totalDocCount, + [fieldValuePair] + ); + + if ( + correlation !== null && + correlation > CORRELATION_THRESHOLD && + ksTest !== null && + ksTest < KS_TEST_THRESHOLD + ) { + const logHistogram = await fetchTransactionDurationRanges( + esClient, + params, + histogramRangeSteps, + [fieldValuePair] + ); + return { + ...fieldValuePair, + correlation, + ksTest, + histogram: logHistogram, + }; + } +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_failure_correlation.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_failure_correlation.ts index 10a098c4a3ffc..cd8d1aacde9ae 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_failure_correlation.ts @@ -6,7 +6,8 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient } from 'kibana/server'; -import { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import { CorrelationsParams } from '../../../../common/correlations/types'; +import { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../../common/event_outcome'; import { fetchTransactionDurationRanges } from './query_ranges'; @@ -14,7 +15,7 @@ import { getQueryWithParams, getTermsQuery } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getFailureCorrelationRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, fieldName: string ): estypes.SearchRequest => { const query = getQueryWithParams({ @@ -65,7 +66,7 @@ export const getFailureCorrelationRequest = ( export const fetchFailedTransactionsCorrelationPValues = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, histogramRangeSteps: number[], fieldName: string ) => { @@ -88,7 +89,7 @@ export const fetchFailedTransactionsCorrelationPValues = async ( }>; // Using for of to sequentially augment the results with histogram data. - const result = []; + const result: FailedTransactionsCorrelation[] = []; for (const bucket of overallResult.buckets) { // Scale the score into a value from 0 - 1 // using a concave piecewise linear function in -log(p-value) diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts similarity index 98% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts index 311016a1b0834..02af6637e5bb3 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.test.ts @@ -10,7 +10,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { hasPrefixToInclude } from '../utils'; +import { hasPrefixToInclude } from '../../../../common/correlations/utils'; import { fetchTransactionDurationFieldCandidates, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.ts index 612225a2348cb..801bb18e8957a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_candidates.ts @@ -11,15 +11,14 @@ import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { ElasticsearchClient } from 'src/core/server'; -import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; - +import type { CorrelationsParams } from '../../../../common/correlations/types'; import { FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE, FIELDS_TO_ADD_AS_CANDIDATE, FIELDS_TO_EXCLUDE_AS_CANDIDATE, POPULATED_DOC_COUNT_SAMPLE_SIZE, -} from '../constants'; -import { hasPrefixToInclude } from '../utils'; +} from '../../../../common/correlations/constants'; +import { hasPrefixToInclude } from '../../../../common/correlations/utils'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; @@ -40,7 +39,7 @@ export const shouldBeExcluded = (fieldName: string) => { }; export const getRandomDocsRequest = ( - params: SearchStrategyParams + params: CorrelationsParams ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -59,7 +58,7 @@ export const getRandomDocsRequest = ( export const fetchTransactionDurationFieldCandidates = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams + params: CorrelationsParams ): Promise<{ fieldCandidates: string[] }> => { const { index } = params; // Get all supported fields diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.test.ts similarity index 81% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.test.ts index bb3aa40b328af..80016930184b3 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.test.ts @@ -10,9 +10,6 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { searchServiceLogProvider } from '../search_service_log'; -import { latencyCorrelationsSearchServiceStateProvider } from '../latency_correlations/latency_correlations_search_service_state'; - import { fetchTransactionDurationFieldValuePairs, getTermsAggRequest, @@ -66,21 +63,14 @@ describe('query_field_value_pairs', () => { search: esClientSearchMock, } as unknown as ElasticsearchClient; - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - const state = latencyCorrelationsSearchServiceStateProvider(); - const resp = await fetchTransactionDurationFieldValuePairs( esClientMock, params, - fieldCandidates, - state, - addLogMessage + fieldCandidates ); - const { progress } = state.getState(); - - expect(progress.loadedFieldValuePairs).toBe(1); - expect(resp).toEqual([ + expect(resp.errors).toEqual([]); + expect(resp.fieldValuePairs).toEqual([ { fieldName: 'myFieldCandidate1', fieldValue: 'myValue1' }, { fieldName: 'myFieldCandidate1', fieldValue: 'myValue2' }, { fieldName: 'myFieldCandidate2', fieldValue: 'myValue1' }, @@ -89,7 +79,6 @@ describe('query_field_value_pairs', () => { { fieldName: 'myFieldCandidate3', fieldValue: 'myValue2' }, ]); expect(esClientSearchMock).toHaveBeenCalledTimes(3); - expect(getLogMessages()).toEqual([]); }); }); }); diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.ts new file mode 100644 index 0000000000000..16c4dacb5ef95 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_field_value_pairs.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 type { ElasticsearchClient } from 'src/core/server'; + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { + FieldValuePair, + CorrelationsParams, +} from '../../../../common/correlations/types'; +import { TERMS_SIZE } from '../../../../common/correlations/constants'; + +import { splitAllSettledPromises } from '../utils'; + +import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; + +export const getTermsAggRequest = ( + params: CorrelationsParams, + fieldName: string +): estypes.SearchRequest => ({ + ...getRequestBase(params), + body: { + query: getQueryWithParams({ params }), + size: 0, + aggs: { + attribute_terms: { + terms: { + field: fieldName, + size: TERMS_SIZE, + }, + }, + }, + }, +}); + +const fetchTransactionDurationFieldTerms = async ( + esClient: ElasticsearchClient, + params: CorrelationsParams, + fieldName: string +): Promise => { + const resp = await esClient.search(getTermsAggRequest(params, fieldName)); + + if (resp.body.aggregations === undefined) { + throw new Error( + 'fetchTransactionDurationFieldTerms failed, did not return aggregations.' + ); + } + + const buckets = ( + resp.body.aggregations + .attribute_terms as estypes.AggregationsMultiBucketAggregate<{ + key: string; + key_as_string?: string; + }> + )?.buckets; + if (buckets?.length >= 1) { + return buckets.map((d) => ({ + fieldName, + // The terms aggregation returns boolean fields as { key: 0, key_as_string: "false" }, + // so we need to pick `key_as_string` if it's present, otherwise searches on boolean fields would fail later on. + fieldValue: d.key_as_string ?? d.key, + })); + } + + return []; +}; + +export const fetchTransactionDurationFieldValuePairs = async ( + esClient: ElasticsearchClient, + params: CorrelationsParams, + fieldCandidates: string[] +): Promise<{ fieldValuePairs: FieldValuePair[]; errors: any[] }> => { + const { fulfilled: responses, rejected: errors } = splitAllSettledPromises( + await Promise.allSettled( + fieldCandidates.map((fieldCandidate) => + fetchTransactionDurationFieldTerms(esClient, params, fieldCandidate) + ) + ) + ); + + return { fieldValuePairs: responses.flat(), errors }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.test.ts similarity index 98% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.test.ts index 5c18b21fc029c..12b054e18bab7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.test.ts @@ -47,6 +47,7 @@ describe('query_fractions', () => { } => { return { body: { + hits: { total: { value: 3 } }, aggregations: { latency_ranges: { buckets: [{ doc_count: 1 }, { doc_count: 2 }], diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.ts similarity index 87% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.ts index 555465466498a..fb9aa0f77b510 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_fractions.ts @@ -8,14 +8,14 @@ import { ElasticsearchClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import { CorrelationsParams } from '../../../../common/correlations/types'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getTransactionDurationRangesRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, ranges: estypes.AggregationsAggregationRange[] ): estypes.SearchRequest => ({ ...getRequestBase(params), @@ -38,12 +38,20 @@ export const getTransactionDurationRangesRequest = ( */ export const fetchTransactionDurationFractions = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, ranges: estypes.AggregationsAggregationRange[] ): Promise<{ fractions: number[]; totalDocCount: number }> => { const resp = await esClient.search( getTransactionDurationRangesRequest(params, ranges) ); + + if ((resp.body.hits.total as estypes.SearchTotalHits).value === 0) { + return { + fractions: [], + totalDocCount: 0, + }; + } + if (resp.body.aggregations === undefined) { throw new Error( 'fetchTransactionDurationFractions failed, did not return aggregations.' diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.ts index 4e40834acccd1..0a96253803ea2 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram.ts @@ -14,14 +14,14 @@ import type { FieldValuePair, HistogramItem, ResponseHit, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getTransactionDurationHistogramRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, interval: number, termFilters?: FieldValuePair[] ): estypes.SearchRequest => ({ @@ -39,7 +39,7 @@ export const getTransactionDurationHistogramRequest = ( export const fetchTransactionDurationHistogram = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, interval: number, termFilters?: FieldValuePair[] ): Promise => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.ts index 176e7befda53b..aa63bcc770c21 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_histogram_range_steps.ts @@ -12,7 +12,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import type { CorrelationsParams } from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; @@ -31,7 +31,7 @@ export const getHistogramRangeSteps = ( }; export const getHistogramIntervalRequest = ( - params: SearchStrategyParams + params: CorrelationsParams ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -46,7 +46,7 @@ export const getHistogramIntervalRequest = ( export const fetchTransactionDurationHistogramRangeSteps = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams + params: CorrelationsParams ): Promise => { const steps = 100; diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_p_values.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_p_values.ts new file mode 100644 index 0000000000000..7c471aebd0f7a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_p_values.ts @@ -0,0 +1,58 @@ +/* + * 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 { ElasticsearchClient } from 'src/core/server'; + +import type { CorrelationsParams } from '../../../../common/correlations/types'; +import type { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; +import { ERROR_CORRELATION_THRESHOLD } from '../../../../common/correlations/constants'; + +import { splitAllSettledPromises } from '../utils'; + +import { + fetchFailedTransactionsCorrelationPValues, + fetchTransactionDurationHistogramRangeSteps, +} from './index'; + +export const fetchPValues = async ( + esClient: ElasticsearchClient, + paramsWithIndex: CorrelationsParams, + fieldCandidates: string[] +) => { + const histogramRangeSteps = await fetchTransactionDurationHistogramRangeSteps( + esClient, + paramsWithIndex + ); + + const { fulfilled, rejected } = splitAllSettledPromises( + await Promise.allSettled( + fieldCandidates.map((fieldName) => + fetchFailedTransactionsCorrelationPValues( + esClient, + paramsWithIndex, + histogramRangeSteps, + fieldName + ) + ) + ) + ); + + const failedTransactionsCorrelations: FailedTransactionsCorrelation[] = + fulfilled + .flat() + .filter( + (record) => + record && + typeof record.pValue === 'number' && + record.pValue < ERROR_CORRELATION_THRESHOLD + ); + + const ccsWarning = + rejected.length > 0 && paramsWithIndex?.index.includes(':'); + + return { failedTransactionsCorrelations, ccsWarning }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.ts index 4e1a7b2015614..68efcadd1bd0b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_percentiles.ts @@ -10,18 +10,18 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { SIGNIFICANT_VALUE_DIGITS } from '../../../../common/correlations/constants'; import type { FieldValuePair, ResponseHit, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -import { SIGNIFICANT_VALUE_DIGITS } from '../constants'; export const getTransactionDurationPercentilesRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, percents?: number[], termFilters?: FieldValuePair[] ): estypes.SearchRequest => { @@ -50,7 +50,7 @@ export const getTransactionDurationPercentilesRequest = ( export const fetchTransactionDurationPercentiles = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, percents?: number[], termFilters?: FieldValuePair[] ): Promise<{ totalDocs: number; percentiles: Record }> => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts rename to x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.ts index 8b359c3665eaf..d35f438046276 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_ranges.ts @@ -13,14 +13,14 @@ import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldname import type { FieldValuePair, ResponseHit, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; + CorrelationsParams, +} from '../../../../common/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getTransactionDurationRangesRequest = ( - params: SearchStrategyParams, + params: CorrelationsParams, rangesSteps: number[], termFilters?: FieldValuePair[] ): estypes.SearchRequest => { @@ -57,7 +57,7 @@ export const getTransactionDurationRangesRequest = ( export const fetchTransactionDurationRanges = async ( esClient: ElasticsearchClient, - params: SearchStrategyParams, + params: CorrelationsParams, rangesSteps: number[], termFilters?: FieldValuePair[] ): Promise> => { diff --git a/x-pack/plugins/apm/server/lib/correlations/queries/query_significant_correlations.ts b/x-pack/plugins/apm/server/lib/correlations/queries/query_significant_correlations.ts new file mode 100644 index 0000000000000..ed5ad1c278143 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/queries/query_significant_correlations.ts @@ -0,0 +1,87 @@ +/* + * 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 { range } from 'lodash'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { + FieldValuePair, + CorrelationsParams, +} from '../../../../common/correlations/types'; +import { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; + +import { + computeExpectationsAndRanges, + splitAllSettledPromises, +} from '../utils'; + +import { + fetchTransactionDurationCorrelationWithHistogram, + fetchTransactionDurationFractions, + fetchTransactionDurationHistogramRangeSteps, + fetchTransactionDurationPercentiles, +} from './index'; + +export const fetchSignificantCorrelations = async ( + esClient: ElasticsearchClient, + paramsWithIndex: CorrelationsParams, + fieldValuePairs: FieldValuePair[] +) => { + // Create an array of ranges [2, 4, 6, ..., 98] + const percentileAggregationPercents = range(2, 100, 2); + const { percentiles: percentilesRecords } = + await fetchTransactionDurationPercentiles( + esClient, + paramsWithIndex, + percentileAggregationPercents + ); + + // We need to round the percentiles values + // because the queries we're using based on it + // later on wouldn't allow numbers with decimals. + const percentiles = Object.values(percentilesRecords).map(Math.round); + + const { expectations, ranges } = computeExpectationsAndRanges(percentiles); + + const { fractions, totalDocCount } = await fetchTransactionDurationFractions( + esClient, + paramsWithIndex, + ranges + ); + + const histogramRangeSteps = await fetchTransactionDurationHistogramRangeSteps( + esClient, + paramsWithIndex + ); + + const { fulfilled, rejected } = splitAllSettledPromises( + await Promise.allSettled( + fieldValuePairs.map((fieldValuePair) => + fetchTransactionDurationCorrelationWithHistogram( + esClient, + paramsWithIndex, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePair + ) + ) + ) + ); + + const latencyCorrelations: LatencyCorrelation[] = fulfilled.filter( + (d): d is LatencyCorrelation => d !== undefined + ); + + const ccsWarning = + rejected.length > 0 && paramsWithIndex?.index.includes(':'); + + return { latencyCorrelations, ccsWarning, totalDocCount }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.test.ts b/x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.test.ts rename to x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.ts b/x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.ts similarity index 79% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.ts rename to x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.ts index 1754a35280f86..1b92133c732cf 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.ts +++ b/x-pack/plugins/apm/server/lib/correlations/utils/compute_expectations_and_ranges.ts @@ -6,7 +6,8 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { PERCENTILES_STEP } from '../constants'; + +import { PERCENTILES_STEP } from '../../../../common/correlations/constants'; export const computeExpectationsAndRanges = ( percentiles: number[], @@ -29,15 +30,17 @@ export const computeExpectationsAndRanges = ( } tempFractions.push(PERCENTILES_STEP / 100); - const ranges = tempPercentiles.reduce((p, to) => { - const from = p[p.length - 1]?.to; - if (from !== undefined) { - p.push({ from, to }); - } else { - p.push({ to }); - } - return p; - }, [] as Array<{ from?: number; to?: number }>); + const ranges = tempPercentiles + .map((tP) => Math.round(tP)) + .reduce((p, to) => { + const from = p[p.length - 1]?.to; + if (from !== undefined) { + p.push({ from, to }); + } else { + p.push({ to }); + } + return p; + }, [] as Array<{ from?: number; to?: number }>); if (ranges.length > 0) { ranges.push({ from: ranges[ranges.length - 1].to }); } diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/field_stats_utils.ts b/x-pack/plugins/apm/server/lib/correlations/utils/field_stats_utils.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/field_stats_utils.ts rename to x-pack/plugins/apm/server/lib/correlations/utils/field_stats_utils.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/utils/index.ts b/x-pack/plugins/apm/server/lib/correlations/utils/index.ts similarity index 82% rename from x-pack/plugins/apm/server/lib/search_strategies/utils/index.ts rename to x-pack/plugins/apm/server/lib/correlations/utils/index.ts index 727bc6cd787a0..f7c5abef939b9 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/utils/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/utils/index.ts @@ -6,4 +6,4 @@ */ export { computeExpectationsAndRanges } from './compute_expectations_and_ranges'; -export { hasPrefixToInclude } from './has_prefix_to_include'; +export { splitAllSettledPromises } from './split_all_settled_promises'; diff --git a/x-pack/plugins/apm/server/lib/correlations/utils/split_all_settled_promises.ts b/x-pack/plugins/apm/server/lib/correlations/utils/split_all_settled_promises.ts new file mode 100644 index 0000000000000..4e060477f024f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/utils/split_all_settled_promises.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. + */ + +interface HandledPromises { + fulfilled: T[]; + rejected: unknown[]; +} + +export const splitAllSettledPromises = ( + promises: Array> +): HandledPromises => + promises.reduce( + (result, current) => { + if (current.status === 'fulfilled') { + result.fulfilled.push(current.value as T); + } else if (current.status === 'rejected') { + result.rejected.push(current.reason); + } + return result; + }, + { + fulfilled: [], + rejected: [], + } as HandledPromises + ); diff --git a/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts index ad1914d921211..0ef6712102a9b 100644 --- a/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts @@ -14,8 +14,8 @@ import { withApmSpan } from '../../utils/with_apm_span'; import { getHistogramIntervalRequest, getHistogramRangeSteps, -} from '../search_strategies/queries/query_histogram_range_steps'; -import { getTransactionDurationRangesRequest } from '../search_strategies/queries/query_ranges'; +} from '../correlations/queries/query_histogram_range_steps'; +import { getTransactionDurationRangesRequest } from '../correlations/queries/query_ranges'; import { getPercentileThresholdValue } from './get_percentile_threshold_value'; import type { @@ -27,9 +27,7 @@ export async function getOverallLatencyDistribution( options: OverallLatencyDistributionOptions ) { return withApmSpan('get_overall_latency_distribution', async () => { - const overallLatencyDistribution: OverallLatencyDistributionResponse = { - log: [], - }; + const overallLatencyDistribution: OverallLatencyDistributionResponse = {}; const { setup, termFilters, ...rawParams } = options; const { apmEventClient } = setup; diff --git a/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts index 996e039841b88..fac22b13a93a8 100644 --- a/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts +++ b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ProcessorEvent } from '../../../common/processor_event'; -import { getTransactionDurationPercentilesRequest } from '../search_strategies/queries/query_percentiles'; +import { getTransactionDurationPercentilesRequest } from '../correlations/queries/query_percentiles'; import type { OverallLatencyDistributionOptions } from './types'; diff --git a/x-pack/plugins/apm/server/lib/latency/types.ts b/x-pack/plugins/apm/server/lib/latency/types.ts index ed7408c297ad7..17c036f44f088 100644 --- a/x-pack/plugins/apm/server/lib/latency/types.ts +++ b/x-pack/plugins/apm/server/lib/latency/types.ts @@ -7,20 +7,19 @@ import type { FieldValuePair, - SearchStrategyClientParams, -} from '../../../common/search_strategies/types'; + CorrelationsClientParams, +} from '../../../common/correlations/types'; import { Setup } from '../helpers/setup_request'; export interface OverallLatencyDistributionOptions - extends SearchStrategyClientParams { + extends CorrelationsClientParams { percentileThreshold: number; termFilters?: FieldValuePair[]; setup: Setup; } export interface OverallLatencyDistributionResponse { - log: string[]; percentileThresholdValue?: number; overallHistogram?: Array<{ key: number; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts deleted file mode 100644 index efc28ce98e5e0..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts +++ /dev/null @@ -1,259 +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 { chunk } from 'lodash'; - -import type { ElasticsearchClient } from 'src/core/server'; - -import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; -import { EventOutcome } from '../../../../common/event_outcome'; -import type { - SearchStrategyClientParams, - SearchStrategyServerParams, - RawResponseBase, -} from '../../../../common/search_strategies/types'; -import type { - FailedTransactionsCorrelationsParams, - FailedTransactionsCorrelationsRawResponse, -} from '../../../../common/search_strategies/failed_transactions_correlations/types'; -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; -import { searchServiceLogProvider } from '../search_service_log'; -import { - fetchFailedTransactionsCorrelationPValues, - fetchTransactionDurationFieldCandidates, - fetchTransactionDurationPercentiles, - fetchTransactionDurationRanges, - fetchTransactionDurationHistogramRangeSteps, -} from '../queries'; -import type { SearchServiceProvider } from '../search_strategy_provider'; - -import { failedTransactionsCorrelationsSearchServiceStateProvider } from './failed_transactions_correlations_search_service_state'; - -import { ERROR_CORRELATION_THRESHOLD } from '../constants'; -import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; - -type FailedTransactionsCorrelationsSearchServiceProvider = - SearchServiceProvider< - FailedTransactionsCorrelationsParams & SearchStrategyClientParams, - FailedTransactionsCorrelationsRawResponse & RawResponseBase - >; - -export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider = - ( - esClient: ElasticsearchClient, - getApmIndices: () => Promise, - searchServiceParams: FailedTransactionsCorrelationsParams & - SearchStrategyClientParams, - includeFrozen: boolean - ) => { - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - const state = failedTransactionsCorrelationsSearchServiceStateProvider(); - - async function fetchErrorCorrelations() { - try { - const indices = await getApmIndices(); - const params: FailedTransactionsCorrelationsParams & - SearchStrategyClientParams & - SearchStrategyServerParams = { - ...searchServiceParams, - index: indices.transaction, - includeFrozen, - }; - - // 95th percentile to be displayed as a marker in the log log chart - const { totalDocs, percentiles: percentilesResponseThresholds } = - await fetchTransactionDurationPercentiles( - esClient, - params, - params.percentileThreshold - ? [params.percentileThreshold] - : undefined - ); - const percentileThresholdValue = - percentilesResponseThresholds[`${params.percentileThreshold}.0`]; - state.setPercentileThresholdValue(percentileThresholdValue); - - addLogMessage( - `Fetched ${params.percentileThreshold}th percentile value of ${percentileThresholdValue} based on ${totalDocs} documents.` - ); - - // finish early if we weren't able to identify the percentileThresholdValue. - if (percentileThresholdValue === undefined) { - addLogMessage( - `Abort service since percentileThresholdValue could not be determined.` - ); - state.setProgress({ - loadedFieldCandidates: 1, - loadedErrorCorrelations: 1, - loadedOverallHistogram: 1, - loadedFailedTransactionsCorrelations: 1, - }); - state.setIsRunning(false); - return; - } - - const histogramRangeSteps = - await fetchTransactionDurationHistogramRangeSteps(esClient, params); - - const overallLogHistogramChartData = - await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps - ); - const errorLogHistogramChartData = await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps, - [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }] - ); - - state.setProgress({ loadedOverallHistogram: 1 }); - state.setErrorHistogram(errorLogHistogramChartData); - state.setOverallHistogram(overallLogHistogramChartData); - - const { fieldCandidates: candidates } = - await fetchTransactionDurationFieldCandidates(esClient, params); - - const fieldCandidates = candidates.filter( - (t) => !(t === EVENT_OUTCOME) - ); - - addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); - - state.setProgress({ loadedFieldCandidates: 1 }); - - let fieldCandidatesFetchedCount = 0; - const fieldsToSample = new Set(); - if (params !== undefined && fieldCandidates.length > 0) { - const batches = chunk(fieldCandidates, 10); - for (let i = 0; i < batches.length; i++) { - try { - const results = await Promise.allSettled( - batches[i].map((fieldName) => - fetchFailedTransactionsCorrelationPValues( - esClient, - params, - histogramRangeSteps, - fieldName - ) - ) - ); - - results.forEach((result, idx) => { - if (result.status === 'fulfilled') { - const significantCorrelations = result.value.filter( - (record) => - record && - record.pValue !== undefined && - record.pValue < ERROR_CORRELATION_THRESHOLD - ); - - significantCorrelations.forEach((r) => { - fieldsToSample.add(r.fieldName); - }); - - state.addFailedTransactionsCorrelations( - significantCorrelations - ); - } else { - // If one of the fields in the batch had an error - addLogMessage( - `Error getting error correlation for field ${batches[i][idx]}: ${result.reason}.` - ); - } - }); - } catch (e) { - state.setError(e); - - if (params?.index.includes(':')) { - state.setCcsWarning(true); - } - } finally { - fieldCandidatesFetchedCount += batches[i].length; - state.setProgress({ - loadedFailedTransactionsCorrelations: - fieldCandidatesFetchedCount / fieldCandidates.length, - }); - } - } - - addLogMessage( - `Identified correlations for ${fieldCandidatesFetchedCount} fields out of ${fieldCandidates.length} candidates.` - ); - } - - addLogMessage( - `Identified ${fieldsToSample.size} fields to sample for field statistics.` - ); - - const { stats: fieldStats } = await fetchFieldsStats( - esClient, - params, - [...fieldsToSample], - [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }] - ); - - addLogMessage( - `Retrieved field statistics for ${fieldStats.length} fields out of ${fieldsToSample.size} fields.` - ); - - state.addFieldStats(fieldStats); - } catch (e) { - state.setError(e); - } - - addLogMessage( - `Identified ${ - state.getState().failedTransactionsCorrelations.length - } significant correlations relating to failed transactions.` - ); - - state.setIsRunning(false); - } - - fetchErrorCorrelations(); - - return () => { - const { - ccsWarning, - error, - isRunning, - overallHistogram, - errorHistogram, - percentileThresholdValue, - progress, - fieldStats, - } = state.getState(); - - return { - cancel: () => { - addLogMessage(`Service cancelled.`); - state.setIsCancelled(true); - }, - error, - meta: { - loaded: Math.round(state.getOverallProgress() * 100), - total: 100, - isRunning, - isPartial: isRunning, - }, - rawResponse: { - ccsWarning, - log: getLogMessages(), - took: Date.now() - progress.started, - failedTransactionsCorrelations: - state.getFailedTransactionsCorrelationsSortedByScore(), - overallHistogram, - errorHistogram, - percentileThresholdValue, - fieldStats, - }, - }; - }; - }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts deleted file mode 100644 index ed0fe5d6e178b..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts +++ /dev/null @@ -1,131 +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 { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types'; - -import type { HistogramItem } from '../../../../common/search_strategies/types'; -import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; - -interface Progress { - started: number; - loadedFieldCandidates: number; - loadedErrorCorrelations: number; - loadedOverallHistogram: number; - loadedFailedTransactionsCorrelations: number; -} - -export const failedTransactionsCorrelationsSearchServiceStateProvider = () => { - let ccsWarning = false; - function setCcsWarning(d: boolean) { - ccsWarning = d; - } - - let error: Error; - function setError(d: Error) { - error = d; - } - - let isCancelled = false; - function setIsCancelled(d: boolean) { - isCancelled = d; - } - - let isRunning = true; - function setIsRunning(d: boolean) { - isRunning = d; - } - - let errorHistogram: HistogramItem[] | undefined; - function setErrorHistogram(d: HistogramItem[]) { - errorHistogram = d; - } - - let overallHistogram: HistogramItem[] | undefined; - function setOverallHistogram(d: HistogramItem[]) { - overallHistogram = d; - } - - let percentileThresholdValue: number; - function setPercentileThresholdValue(d: number) { - percentileThresholdValue = d; - } - - let progress: Progress = { - started: Date.now(), - loadedFieldCandidates: 0, - loadedErrorCorrelations: 0, - loadedOverallHistogram: 0, - loadedFailedTransactionsCorrelations: 0, - }; - function getOverallProgress() { - return ( - progress.loadedFieldCandidates * 0.025 + - progress.loadedFailedTransactionsCorrelations * (1 - 0.025) - ); - } - function setProgress(d: Partial>) { - progress = { - ...progress, - ...d, - }; - } - - const fieldStats: FieldStats[] = []; - function addFieldStats(stats: FieldStats[]) { - fieldStats.push(...stats); - } - - const failedTransactionsCorrelations: FailedTransactionsCorrelation[] = []; - function addFailedTransactionsCorrelation(d: FailedTransactionsCorrelation) { - failedTransactionsCorrelations.push(d); - } - function addFailedTransactionsCorrelations( - d: FailedTransactionsCorrelation[] - ) { - failedTransactionsCorrelations.push(...d); - } - - function getFailedTransactionsCorrelationsSortedByScore() { - return failedTransactionsCorrelations.sort((a, b) => b.score - a.score); - } - - function getState() { - return { - ccsWarning, - error, - isCancelled, - isRunning, - overallHistogram, - errorHistogram, - percentileThresholdValue, - progress, - failedTransactionsCorrelations, - fieldStats, - }; - } - - return { - addFailedTransactionsCorrelation, - addFailedTransactionsCorrelations, - getOverallProgress, - getState, - getFailedTransactionsCorrelationsSortedByScore, - setCcsWarning, - setError, - setIsCancelled, - setIsRunning, - setOverallHistogram, - setErrorHistogram, - setPercentileThresholdValue, - setProgress, - addFieldStats, - }; -}; - -export type FailedTransactionsCorrelationsSearchServiceState = ReturnType< - typeof failedTransactionsCorrelationsSearchServiceStateProvider ->; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/index.ts deleted file mode 100644 index b4668138eefab..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/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 { registerSearchStrategies } from './register_search_strategies'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts deleted file mode 100644 index 040aa5a7e424e..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/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 { latencyCorrelationsSearchServiceProvider } from './latency_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts deleted file mode 100644 index 5fed2f4eb4dc4..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts +++ /dev/null @@ -1,293 +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 { range } from 'lodash'; -import type { ElasticsearchClient } from 'src/core/server'; - -import type { - RawResponseBase, - SearchStrategyClientParams, - SearchStrategyServerParams, -} from '../../../../common/search_strategies/types'; -import type { - LatencyCorrelationsParams, - LatencyCorrelationsRawResponse, -} from '../../../../common/search_strategies/latency_correlations/types'; - -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; - -import { - fetchTransactionDurationFieldCandidates, - fetchTransactionDurationFieldValuePairs, - fetchTransactionDurationFractions, - fetchTransactionDurationPercentiles, - fetchTransactionDurationHistograms, - fetchTransactionDurationHistogramRangeSteps, - fetchTransactionDurationRanges, -} from '../queries'; -import { computeExpectationsAndRanges } from '../utils'; -import { searchServiceLogProvider } from '../search_service_log'; -import type { SearchServiceProvider } from '../search_strategy_provider'; - -import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; -import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; - -type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< - LatencyCorrelationsParams & SearchStrategyClientParams, - LatencyCorrelationsRawResponse & RawResponseBase ->; - -export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearchServiceProvider = - ( - esClient: ElasticsearchClient, - getApmIndices: () => Promise, - searchServiceParams: LatencyCorrelationsParams & SearchStrategyClientParams, - includeFrozen: boolean - ) => { - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - const state = latencyCorrelationsSearchServiceStateProvider(); - - async function fetchCorrelations() { - let params: - | (LatencyCorrelationsParams & - SearchStrategyClientParams & - SearchStrategyServerParams) - | undefined; - - try { - const indices = await getApmIndices(); - params = { - ...searchServiceParams, - index: indices.transaction, - includeFrozen, - }; - - // 95th percentile to be displayed as a marker in the log log chart - const { totalDocs, percentiles: percentilesResponseThresholds } = - await fetchTransactionDurationPercentiles( - esClient, - params, - params.percentileThreshold - ? [params.percentileThreshold] - : undefined - ); - const percentileThresholdValue = - percentilesResponseThresholds[`${params.percentileThreshold}.0`]; - state.setPercentileThresholdValue(percentileThresholdValue); - - addLogMessage( - `Fetched ${params.percentileThreshold}th percentile value of ${percentileThresholdValue} based on ${totalDocs} documents.` - ); - - // finish early if we weren't able to identify the percentileThresholdValue. - if (percentileThresholdValue === undefined) { - addLogMessage( - `Abort service since percentileThresholdValue could not be determined.` - ); - state.setProgress({ - loadedHistogramStepsize: 1, - loadedOverallHistogram: 1, - loadedFieldCandidates: 1, - loadedFieldValuePairs: 1, - loadedHistograms: 1, - }); - state.setIsRunning(false); - return; - } - - const histogramRangeSteps = - await fetchTransactionDurationHistogramRangeSteps(esClient, params); - state.setProgress({ loadedHistogramStepsize: 1 }); - - addLogMessage(`Loaded histogram range steps.`); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - const overallLogHistogramChartData = - await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps - ); - state.setProgress({ loadedOverallHistogram: 1 }); - state.setOverallHistogram(overallLogHistogramChartData); - - addLogMessage(`Loaded overall histogram chart data.`); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - // finish early if correlation analysis is not required. - if (params.analyzeCorrelations === false) { - addLogMessage( - `Finish service since correlation analysis wasn't requested.` - ); - state.setProgress({ - loadedHistogramStepsize: 1, - loadedOverallHistogram: 1, - loadedFieldCandidates: 1, - loadedFieldValuePairs: 1, - loadedHistograms: 1, - }); - state.setIsRunning(false); - return; - } - - // Create an array of ranges [2, 4, 6, ..., 98] - const percentileAggregationPercents = range(2, 100, 2); - const { percentiles: percentilesRecords } = - await fetchTransactionDurationPercentiles( - esClient, - params, - percentileAggregationPercents - ); - - // We need to round the percentiles values - // because the queries we're using based on it - // later on wouldn't allow numbers with decimals. - const percentiles = Object.values(percentilesRecords).map(Math.round); - - addLogMessage(`Loaded percentiles.`); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - const { fieldCandidates } = - await fetchTransactionDurationFieldCandidates(esClient, params); - - addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); - - state.setProgress({ loadedFieldCandidates: 1 }); - - const fieldValuePairs = await fetchTransactionDurationFieldValuePairs( - esClient, - params, - fieldCandidates, - state, - addLogMessage - ); - - addLogMessage(`Identified ${fieldValuePairs.length} fieldValuePairs.`); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - const { expectations, ranges } = - computeExpectationsAndRanges(percentiles); - - const { fractions, totalDocCount } = - await fetchTransactionDurationFractions(esClient, params, ranges); - - addLogMessage( - `Loaded fractions and totalDocCount of ${totalDocCount}.` - ); - - const fieldsToSample = new Set(); - let loadedHistograms = 0; - for await (const item of fetchTransactionDurationHistograms( - esClient, - addLogMessage, - params, - state, - expectations, - ranges, - fractions, - histogramRangeSteps, - totalDocCount, - fieldValuePairs - )) { - if (item !== undefined) { - state.addLatencyCorrelation(item); - fieldsToSample.add(item.fieldName); - } - loadedHistograms++; - state.setProgress({ - loadedHistograms: loadedHistograms / fieldValuePairs.length, - }); - } - - addLogMessage( - `Identified ${ - state.getState().latencyCorrelations.length - } significant correlations out of ${ - fieldValuePairs.length - } field/value pairs.` - ); - - addLogMessage( - `Identified ${fieldsToSample.size} fields to sample for field statistics.` - ); - - const { stats: fieldStats } = await fetchFieldsStats(esClient, params, [ - ...fieldsToSample, - ]); - - addLogMessage( - `Retrieved field statistics for ${fieldStats.length} fields out of ${fieldsToSample.size} fields.` - ); - state.addFieldStats(fieldStats); - } catch (e) { - state.setError(e); - } - - if (state.getState().error !== undefined && params?.index.includes(':')) { - state.setCcsWarning(true); - } - - state.setIsRunning(false); - } - - function cancel() { - addLogMessage(`Service cancelled.`); - state.setIsCancelled(true); - } - - fetchCorrelations(); - - return () => { - const { - ccsWarning, - error, - isRunning, - overallHistogram, - percentileThresholdValue, - progress, - fieldStats, - } = state.getState(); - - return { - cancel, - error, - meta: { - loaded: Math.round(state.getOverallProgress() * 100), - total: 100, - isRunning, - isPartial: isRunning, - }, - rawResponse: { - ccsWarning, - log: getLogMessages(), - took: Date.now() - progress.started, - latencyCorrelations: - state.getLatencyCorrelationsSortedByCorrelation(), - percentileThresholdValue, - overallHistogram, - fieldStats, - }, - }; - }; - }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts deleted file mode 100644 index ce9014004f4b0..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; - -describe('search service', () => { - describe('latencyCorrelationsSearchServiceStateProvider', () => { - it('initializes with default state', () => { - const state = latencyCorrelationsSearchServiceStateProvider(); - const defaultState = state.getState(); - const defaultProgress = state.getOverallProgress(); - - expect(defaultState.ccsWarning).toBe(false); - expect(defaultState.error).toBe(undefined); - expect(defaultState.isCancelled).toBe(false); - expect(defaultState.isRunning).toBe(true); - expect(defaultState.overallHistogram).toBe(undefined); - expect(defaultState.progress.loadedFieldCandidates).toBe(0); - expect(defaultState.progress.loadedFieldValuePairs).toBe(0); - expect(defaultState.progress.loadedHistogramStepsize).toBe(0); - expect(defaultState.progress.loadedHistograms).toBe(0); - expect(defaultState.progress.loadedOverallHistogram).toBe(0); - expect(defaultState.progress.started > 0).toBe(true); - - expect(defaultProgress).toBe(0); - }); - - it('returns updated state', () => { - const state = latencyCorrelationsSearchServiceStateProvider(); - - state.setCcsWarning(true); - state.setError(new Error('the-error-message')); - state.setIsCancelled(true); - state.setIsRunning(false); - state.setOverallHistogram([{ key: 1392202800000, doc_count: 1234 }]); - state.setProgress({ loadedHistograms: 0.5 }); - - const updatedState = state.getState(); - const updatedProgress = state.getOverallProgress(); - - expect(updatedState.ccsWarning).toBe(true); - expect(updatedState.error?.message).toBe('the-error-message'); - expect(updatedState.isCancelled).toBe(true); - expect(updatedState.isRunning).toBe(false); - expect(updatedState.overallHistogram).toEqual([ - { key: 1392202800000, doc_count: 1234 }, - ]); - expect(updatedState.progress.loadedFieldCandidates).toBe(0); - expect(updatedState.progress.loadedFieldValuePairs).toBe(0); - expect(updatedState.progress.loadedHistogramStepsize).toBe(0); - expect(updatedState.progress.loadedHistograms).toBe(0.5); - expect(updatedState.progress.loadedOverallHistogram).toBe(0); - expect(updatedState.progress.started > 0).toBe(true); - - expect(updatedProgress).toBe(0.45); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts deleted file mode 100644 index 186099e4c307a..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts +++ /dev/null @@ -1,121 +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 { HistogramItem } from '../../../../common/search_strategies/types'; -import type { - LatencyCorrelationSearchServiceProgress, - LatencyCorrelation, -} from '../../../../common/search_strategies/latency_correlations/types'; -import { FieldStats } from '../../../../common/search_strategies/field_stats_types'; - -export const latencyCorrelationsSearchServiceStateProvider = () => { - let ccsWarning = false; - function setCcsWarning(d: boolean) { - ccsWarning = d; - } - - let error: Error; - function setError(d: Error) { - error = d; - } - - let isCancelled = false; - function getIsCancelled() { - return isCancelled; - } - function setIsCancelled(d: boolean) { - isCancelled = d; - } - - let isRunning = true; - function setIsRunning(d: boolean) { - isRunning = d; - } - - let overallHistogram: HistogramItem[] | undefined; - function setOverallHistogram(d: HistogramItem[]) { - overallHistogram = d; - } - - let percentileThresholdValue: number; - function setPercentileThresholdValue(d: number) { - percentileThresholdValue = d; - } - - let progress: LatencyCorrelationSearchServiceProgress = { - started: Date.now(), - loadedHistogramStepsize: 0, - loadedOverallHistogram: 0, - loadedFieldCandidates: 0, - loadedFieldValuePairs: 0, - loadedHistograms: 0, - }; - function getOverallProgress() { - return ( - progress.loadedHistogramStepsize * 0.025 + - progress.loadedOverallHistogram * 0.025 + - progress.loadedFieldCandidates * 0.025 + - progress.loadedFieldValuePairs * 0.025 + - progress.loadedHistograms * 0.9 - ); - } - function setProgress( - d: Partial> - ) { - progress = { - ...progress, - ...d, - }; - } - - const latencyCorrelations: LatencyCorrelation[] = []; - function addLatencyCorrelation(d: LatencyCorrelation) { - latencyCorrelations.push(d); - } - - function getLatencyCorrelationsSortedByCorrelation() { - return latencyCorrelations.sort((a, b) => b.correlation - a.correlation); - } - const fieldStats: FieldStats[] = []; - function addFieldStats(stats: FieldStats[]) { - fieldStats.push(...stats); - } - - function getState() { - return { - ccsWarning, - error, - isCancelled, - isRunning, - overallHistogram, - percentileThresholdValue, - progress, - latencyCorrelations, - fieldStats, - }; - } - - return { - addLatencyCorrelation, - getIsCancelled, - getOverallProgress, - getState, - getLatencyCorrelationsSortedByCorrelation, - setCcsWarning, - setError, - setIsCancelled, - setIsRunning, - setOverallHistogram, - setPercentileThresholdValue, - setProgress, - addFieldStats, - }; -}; - -export type LatencyCorrelationsSearchServiceState = ReturnType< - typeof latencyCorrelationsSearchServiceStateProvider ->; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts deleted file mode 100644 index e57ef5ee341ee..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts +++ /dev/null @@ -1,124 +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 { ElasticsearchClient } from 'src/core/server'; - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { - FieldValuePair, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; - -import type { SearchServiceLog } from '../search_service_log'; -import type { LatencyCorrelationsSearchServiceState } from '../latency_correlations/latency_correlations_search_service_state'; -import { TERMS_SIZE } from '../constants'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getTermsAggRequest = ( - params: SearchStrategyParams, - fieldName: string -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - attribute_terms: { - terms: { - field: fieldName, - size: TERMS_SIZE, - }, - }, - }, - }, -}); - -const fetchTransactionDurationFieldTerms = async ( - esClient: ElasticsearchClient, - params: SearchStrategyParams, - fieldName: string, - addLogMessage: SearchServiceLog['addLogMessage'] -): Promise => { - try { - const resp = await esClient.search(getTermsAggRequest(params, fieldName)); - - if (resp.body.aggregations === undefined) { - addLogMessage( - `Failed to fetch terms for field candidate ${fieldName} fieldValuePairs, no aggregations returned.`, - JSON.stringify(resp) - ); - return []; - } - const buckets = ( - resp.body.aggregations - .attribute_terms as estypes.AggregationsMultiBucketAggregate<{ - key: string; - key_as_string?: string; - }> - )?.buckets; - if (buckets?.length >= 1) { - return buckets.map((d) => ({ - fieldName, - // The terms aggregation returns boolean fields as { key: 0, key_as_string: "false" }, - // so we need to pick `key_as_string` if it's present, otherwise searches on boolean fields would fail later on. - fieldValue: d.key_as_string ?? d.key, - })); - } - } catch (e) { - addLogMessage( - `Failed to fetch terms for field candidate ${fieldName} fieldValuePairs.`, - JSON.stringify(e) - ); - } - - return []; -}; - -async function fetchInSequence( - fieldCandidates: string[], - fn: (fieldCandidate: string) => Promise -) { - const results = []; - - for (const fieldCandidate of fieldCandidates) { - results.push(...(await fn(fieldCandidate))); - } - - return results; -} - -export const fetchTransactionDurationFieldValuePairs = async ( - esClient: ElasticsearchClient, - params: SearchStrategyParams, - fieldCandidates: string[], - state: LatencyCorrelationsSearchServiceState, - addLogMessage: SearchServiceLog['addLogMessage'] -): Promise => { - let fieldValuePairsProgress = 1; - - return await fetchInSequence( - fieldCandidates, - async function (fieldCandidate: string) { - const fieldTerms = await fetchTransactionDurationFieldTerms( - esClient, - params, - fieldCandidate, - addLogMessage - ); - - state.setProgress({ - loadedFieldValuePairs: fieldValuePairsProgress / fieldCandidates.length, - }); - fieldValuePairsProgress++; - - return fieldTerms; - } - ); -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts deleted file mode 100644 index 500714ffdf0d5..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts +++ /dev/null @@ -1,96 +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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from 'src/core/server'; - -import type { - FieldValuePair, - SearchStrategyParams, -} from '../../../../common/search_strategies/types'; - -import type { SearchServiceLog } from '../search_service_log'; -import type { LatencyCorrelationsSearchServiceState } from '../latency_correlations/latency_correlations_search_service_state'; -import { CORRELATION_THRESHOLD, KS_TEST_THRESHOLD } from '../constants'; - -import { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; -import { fetchTransactionDurationCorrelation } from './query_correlation'; -import { fetchTransactionDurationRanges } from './query_ranges'; - -export async function* fetchTransactionDurationHistograms( - esClient: ElasticsearchClient, - addLogMessage: SearchServiceLog['addLogMessage'], - params: SearchStrategyParams, - state: LatencyCorrelationsSearchServiceState, - expectations: number[], - ranges: estypes.AggregationsAggregationRange[], - fractions: number[], - histogramRangeSteps: number[], - totalDocCount: number, - fieldValuePairs: FieldValuePair[] -) { - for (const item of getPrioritizedFieldValuePairs(fieldValuePairs)) { - if (params === undefined || item === undefined || state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - // If one of the fields have an error - // We don't want to stop the whole process - try { - const { correlation, ksTest } = await fetchTransactionDurationCorrelation( - esClient, - params, - expectations, - ranges, - fractions, - totalDocCount, - [item] - ); - - if (state.getIsCancelled()) { - state.setIsRunning(false); - return; - } - - if ( - correlation !== null && - correlation > CORRELATION_THRESHOLD && - ksTest !== null && - ksTest < KS_TEST_THRESHOLD - ) { - const logHistogram = await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps, - [item] - ); - yield { - ...item, - correlation, - ksTest, - histogram: logHistogram, - }; - } else { - yield undefined; - } - } catch (e) { - // don't fail the whole process for individual correlation queries, - // just add the error to the internal log and check if we'd want to set the - // cross-cluster search compatibility warning to true. - addLogMessage( - `Failed to fetch correlation/kstest for '${item.fieldName}/${item.fieldValue}'`, - JSON.stringify(e) - ); - if (params?.index.includes(':')) { - state.setCcsWarning(true); - } - yield undefined; - } - } -} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts b/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts deleted file mode 100644 index 713c5e390ca8b..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts +++ /dev/null @@ -1,40 +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 { PluginSetup as DataPluginSetup } from 'src/plugins/data/server'; - -import { APM_SEARCH_STRATEGIES } from '../../../common/search_strategies/constants'; - -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -import { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations'; -import { latencyCorrelationsSearchServiceProvider } from './latency_correlations'; -import { searchStrategyProvider } from './search_strategy_provider'; - -export const registerSearchStrategies = ( - registerSearchStrategy: DataPluginSetup['search']['registerSearchStrategy'], - getApmIndices: () => Promise, - includeFrozen: boolean -) => { - registerSearchStrategy( - APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, - searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - getApmIndices, - includeFrozen - ) - ); - - registerSearchStrategy( - APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, - searchStrategyProvider( - failedTransactionsCorrelationsSearchServiceProvider, - getApmIndices, - includeFrozen - ) - ); -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts deleted file mode 100644 index 5b887f15a584e..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts +++ /dev/null @@ -1,47 +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 { - searchServiceLogProvider, - currentTimeAsString, -} from './search_service_log'; - -describe('search service', () => { - describe('currentTimeAsString', () => { - it('returns the current time as a string', () => { - const mockDate = new Date(1392202800000); - // @ts-ignore ignore the mockImplementation callback error - const spy = jest.spyOn(global, 'Date').mockReturnValue(mockDate); - - const timeString = currentTimeAsString(); - - expect(timeString).toEqual('2014-02-12T11:00:00.000Z'); - - spy.mockRestore(); - }); - }); - - describe('searchServiceLogProvider', () => { - it('adds and retrieves messages from the log', async () => { - const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - - const mockDate = new Date(1392202800000); - // @ts-ignore ignore the mockImplementation callback error - const spy = jest.spyOn(global, 'Date').mockReturnValue(mockDate); - - addLogMessage('the first message'); - addLogMessage('the second message'); - - expect(getLogMessages()).toEqual([ - '2014-02-12T11:00:00.000Z: the first message', - '2014-02-12T11:00:00.000Z: the second message', - ]); - - spy.mockRestore(); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts deleted file mode 100644 index 73a59021b01ed..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts +++ /dev/null @@ -1,34 +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. - */ - -interface LogMessage { - timestamp: string; - message: string; - error?: string; -} - -export const currentTimeAsString = () => new Date().toISOString(); - -export const searchServiceLogProvider = () => { - const log: LogMessage[] = []; - - function addLogMessage(message: string, error?: string) { - log.push({ - timestamp: currentTimeAsString(), - message, - ...(error !== undefined ? { error } : {}), - }); - } - - function getLogMessages() { - return log.map((l) => `${l.timestamp}: ${l.message}`); - } - - return { addLogMessage, getLogMessages }; -}; - -export type SearchServiceLog = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts deleted file mode 100644 index ccccdeab5132d..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts +++ /dev/null @@ -1,302 +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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { SearchStrategyDependencies } from 'src/plugins/data/server'; - -import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common'; - -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; -import type { LatencyCorrelationsParams } from '../../../common/search_strategies/latency_correlations/types'; -import type { RawSearchStrategyClientParams } from '../../../common/search_strategies/types'; - -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -import { latencyCorrelationsSearchServiceProvider } from './latency_correlations'; -import { searchStrategyProvider } from './search_strategy_provider'; - -// helper to trigger promises in the async search service -const flushPromises = () => new Promise(setImmediate); - -const clientFieldCapsMock = () => ({ body: { fields: [] } }); - -// minimal client mock to fulfill search requirements of the async search service to succeed -const clientSearchMock = ( - req: estypes.SearchRequest -): { body: estypes.SearchResponse } => { - let aggregations: - | { - transaction_duration_percentiles: estypes.AggregationsTDigestPercentilesAggregate; - } - | { - transaction_duration_min: estypes.AggregationsValueAggregate; - transaction_duration_max: estypes.AggregationsValueAggregate; - } - | { - logspace_ranges: estypes.AggregationsMultiBucketAggregate<{ - from: number; - doc_count: number; - }>; - } - | { - latency_ranges: estypes.AggregationsMultiBucketAggregate<{ - doc_count: number; - }>; - } - | undefined; - - if (req?.body?.aggs !== undefined) { - const aggs = req.body.aggs; - // fetchTransactionDurationPercentiles - if (aggs.transaction_duration_percentiles !== undefined) { - aggregations = { transaction_duration_percentiles: { values: {} } }; - } - - // fetchTransactionDurationCorrelation - if (aggs.logspace_ranges !== undefined) { - aggregations = { logspace_ranges: { buckets: [] } }; - } - - // fetchTransactionDurationFractions - if (aggs.latency_ranges !== undefined) { - aggregations = { latency_ranges: { buckets: [] } }; - } - } - - return { - body: { - _shards: { - failed: 0, - successful: 1, - total: 1, - }, - took: 162, - timed_out: false, - hits: { - hits: [], - total: { - value: 0, - relation: 'eq', - }, - }, - ...(aggregations !== undefined ? { aggregations } : {}), - }, - }; -}; - -const getApmIndicesMock = async () => - ({ transaction: 'apm-*' } as ApmIndicesConfig); - -describe('APM Correlations search strategy', () => { - describe('strategy interface', () => { - it('returns a custom search strategy with a `search` and `cancel` function', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - getApmIndicesMock, - false - ); - expect(typeof searchStrategy.search).toBe('function'); - expect(typeof searchStrategy.cancel).toBe('function'); - }); - }); - - describe('search', () => { - let mockClientFieldCaps: jest.Mock; - let mockClientSearch: jest.Mock; - let mockGetApmIndicesMock: jest.Mock; - let mockDeps: SearchStrategyDependencies; - let params: Required< - IKibanaSearchRequest< - LatencyCorrelationsParams & RawSearchStrategyClientParams - > - >['params']; - - beforeEach(() => { - mockClientFieldCaps = jest.fn(clientFieldCapsMock); - mockClientSearch = jest.fn(clientSearchMock); - mockGetApmIndicesMock = jest.fn(getApmIndicesMock); - mockDeps = { - esClient: { - asCurrentUser: { - fieldCaps: mockClientFieldCaps, - search: mockClientSearch, - }, - }, - } as unknown as SearchStrategyDependencies; - params = { - start: '2020', - end: '2021', - environment: ENVIRONMENT_ALL.value, - kuery: '', - percentileThreshold: 95, - analyzeCorrelations: true, - }; - }); - - describe('async functionality', () => { - describe('when no params are provided', () => { - it('throws an error', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(0); - - expect(() => searchStrategy.search({}, {}, mockDeps)).toThrow( - 'Invalid request parameters.' - ); - }); - }); - - describe('when no ID is provided', () => { - it('performs a client search with params', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - await searchStrategy.search({ params }, {}, mockDeps).toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - - const [[request]] = mockClientSearch.mock.calls; - - expect(request.index).toEqual('apm-*'); - expect(request.body).toEqual( - expect.objectContaining({ - aggs: { - transaction_duration_percentiles: { - percentiles: { - field: 'transaction.duration.us', - hdr: { number_of_significant_value_digits: 3 }, - percents: [95], - }, - }, - }, - query: { - bool: { - filter: [ - { term: { 'processor.event': 'transaction' } }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - size: 0, - track_total_hits: true, - }) - ); - }); - }); - - describe('when an ID with params is provided', () => { - it('retrieves the current request', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - const response = await searchStrategy - .search({ params }, {}, mockDeps) - .toPromise(); - - const searchStrategyId = response.id; - - const response2 = await searchStrategy - .search({ id: searchStrategyId, params }, {}, mockDeps) - .toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - expect(response2).toEqual( - expect.objectContaining({ id: searchStrategyId }) - ); - }); - }); - - describe('if the client throws', () => { - it('does not emit an error', async () => { - mockClientSearch - .mockReset() - .mockRejectedValueOnce(new Error('client error')); - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - const response = await searchStrategy - .search({ params }, {}, mockDeps) - .toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - - expect(response).toEqual( - expect.objectContaining({ isRunning: true }) - ); - }); - }); - - it('triggers the subscription only once', async () => { - expect.assertions(2); - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - searchStrategy - .search({ params }, {}, mockDeps) - .subscribe((response) => { - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - expect(response).toEqual( - expect.objectContaining({ loaded: 0, isRunning: true }) - ); - }); - }); - }); - - describe('response', () => { - it('sends an updated response on consecutive search calls', async () => { - const searchStrategy = await searchStrategyProvider( - latencyCorrelationsSearchServiceProvider, - mockGetApmIndicesMock, - false - ); - - const response1 = await searchStrategy - .search({ params }, {}, mockDeps) - .toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - expect(typeof response1.id).toEqual('string'); - expect(response1).toEqual( - expect.objectContaining({ loaded: 0, isRunning: true }) - ); - - await flushPromises(); - - const response2 = await searchStrategy - .search({ id: response1.id, params }, {}, mockDeps) - .toPromise(); - - expect(mockGetApmIndicesMock).toHaveBeenCalledTimes(1); - expect(response2.id).toEqual(response1.id); - expect(response2).toEqual( - expect.objectContaining({ loaded: 100, isRunning: false }) - ); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts deleted file mode 100644 index 8035e9e4d97ca..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts +++ /dev/null @@ -1,204 +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 uuid from 'uuid'; -import { of } from 'rxjs'; -import { getOrElse } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as t from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; - -import type { ElasticsearchClient } from 'src/core/server'; - -import type { ISearchStrategy } from '../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../src/plugins/data/common'; - -import type { - RawResponseBase, - RawSearchStrategyClientParams, - SearchStrategyClientParams, -} from '../../../common/search_strategies/types'; -import type { - LatencyCorrelationsParams, - LatencyCorrelationsRawResponse, -} from '../../../common/search_strategies/latency_correlations/types'; -import type { - FailedTransactionsCorrelationsParams, - FailedTransactionsCorrelationsRawResponse, -} from '../../../common/search_strategies/failed_transactions_correlations/types'; -import { rangeRt } from '../../routes/default_api_types'; -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - -interface SearchServiceState { - cancel: () => void; - error: Error; - meta: { - loaded: number; - total: number; - isRunning: boolean; - isPartial: boolean; - }; - rawResponse: TRawResponse; -} - -type GetSearchServiceState = - () => SearchServiceState; - -export type SearchServiceProvider< - TSearchStrategyClientParams extends SearchStrategyClientParams, - TRawResponse extends RawResponseBase -> = ( - esClient: ElasticsearchClient, - getApmIndices: () => Promise, - searchServiceParams: TSearchStrategyClientParams, - includeFrozen: boolean -) => GetSearchServiceState; - -// Failed Transactions Correlations function overload -export function searchStrategyProvider( - searchServiceProvider: SearchServiceProvider< - FailedTransactionsCorrelationsParams & SearchStrategyClientParams, - FailedTransactionsCorrelationsRawResponse & RawResponseBase - >, - getApmIndices: () => Promise, - includeFrozen: boolean -): ISearchStrategy< - IKibanaSearchRequest< - FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams - >, - IKibanaSearchResponse< - FailedTransactionsCorrelationsRawResponse & RawResponseBase - > ->; - -// Latency Correlations function overload -export function searchStrategyProvider( - searchServiceProvider: SearchServiceProvider< - LatencyCorrelationsParams & SearchStrategyClientParams, - LatencyCorrelationsRawResponse & RawResponseBase - >, - getApmIndices: () => Promise, - includeFrozen: boolean -): ISearchStrategy< - IKibanaSearchRequest< - LatencyCorrelationsParams & RawSearchStrategyClientParams - >, - IKibanaSearchResponse ->; - -export function searchStrategyProvider( - searchServiceProvider: SearchServiceProvider< - TRequestParams & SearchStrategyClientParams, - TResponseParams & RawResponseBase - >, - getApmIndices: () => Promise, - includeFrozen: boolean -): ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse -> { - const searchServiceMap = new Map< - string, - GetSearchServiceState - >(); - - return { - search: (request, options, deps) => { - if (request.params === undefined) { - throw new Error('Invalid request parameters.'); - } - - const { start: startString, end: endString } = request.params; - - // converts string based start/end to epochmillis - const decodedRange = pipe( - rangeRt.decode({ start: startString, end: endString }), - getOrElse((errors) => { - throw new Error(failure(errors).join('\n')); - }) - ); - - // The function to fetch the current state of the search service. - // This will be either an existing service for a follow up fetch or a new one for new requests. - let getSearchServiceState: GetSearchServiceState< - TResponseParams & RawResponseBase - >; - - // If the request includes an ID, we require that the search service already exists - // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. - // This also avoids instantiating search services when the service gets called with random IDs. - if (typeof request.id === 'string') { - const existingGetSearchServiceState = searchServiceMap.get(request.id); - - if (typeof existingGetSearchServiceState === 'undefined') { - throw new Error( - `SearchService with ID '${request.id}' does not exist.` - ); - } - - getSearchServiceState = existingGetSearchServiceState; - } else { - const { - start, - end, - environment, - kuery, - serviceName, - transactionName, - transactionType, - ...requestParams - } = request.params; - - getSearchServiceState = searchServiceProvider( - deps.esClient.asCurrentUser, - getApmIndices, - { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start: decodedRange.start, - end: decodedRange.end, - ...(requestParams as unknown as TRequestParams), - }, - includeFrozen - ); - } - - // Reuse the request's id or create a new one. - const id = request.id ?? uuid(); - - const { error, meta, rawResponse } = getSearchServiceState(); - - if (error instanceof Error) { - searchServiceMap.delete(id); - throw error; - } else if (meta.isRunning) { - searchServiceMap.set(id, getSearchServiceState); - } else { - searchServiceMap.delete(id); - } - - return of({ - id, - ...meta, - rawResponse, - }); - }, - cancel: async (id, options, deps) => { - const getSearchServiceState = searchServiceMap.get(id); - if (getSearchServiceState !== undefined) { - getSearchServiceState().cancel(); - searchServiceMap.delete(id); - } - }, - }; -} diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 72a1bc483015e..4e2ee4f37a8e6 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -15,7 +15,6 @@ import { PluginInitializerContext, } from 'src/core/server'; import { isEmpty, mapValues } from 'lodash'; -import { SavedObjectsClient } from '../../../../src/core/server'; import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; import { Dataset } from '../../rule_registry/server'; import { APMConfig, APM_SERVER_FEATURE_ID } from '.'; @@ -26,7 +25,6 @@ import { registerFleetPolicyCallbacks } from './lib/fleet/register_fleet_policy_ import { createApmTelemetry } from './lib/apm_telemetry'; import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; -import { registerSearchStrategies } from './lib/search_strategies'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index'; @@ -197,25 +195,6 @@ export class APMPlugin logger: this.logger, }); - // search strategies for async partial search results - core.getStartServices().then(([coreStart]) => { - (async () => { - const savedObjectsClient = new SavedObjectsClient( - coreStart.savedObjects.createInternalRepository() - ); - - const includeFrozen = await coreStart.uiSettings - .asScopedToClient(savedObjectsClient) - .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - - registerSearchStrategies( - plugins.data.search.registerSearchStrategy, - boundGetApmIndices, - includeFrozen - ); - })(); - }); - core.deprecations.registerDeprecations({ getDeprecations: getDeprecations({ cloudSetup: plugins.cloud, diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts new file mode 100644 index 0000000000000..8b20d57d25d67 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -0,0 +1,256 @@ +/* + * 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 Boom from '@hapi/boom'; + +import { i18n } from '@kbn/i18n'; +import { toNumberRt } from '@kbn/io-ts-utils'; + +import { isActivePlatinumLicense } from '../../common/license_check'; + +import { setupRequest } from '../lib/helpers/setup_request'; +import { + fetchPValues, + fetchSignificantCorrelations, + fetchTransactionDurationFieldCandidates, + fetchTransactionDurationFieldValuePairs, +} from '../lib/correlations/queries'; +import { fetchFieldsStats } from '../lib/correlations/queries/field_stats/get_fields_stats'; + +import { withApmSpan } from '../utils/with_apm_span'; + +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { environmentRt, kueryRt, rangeRt } from './default_api_types'; + +const INVALID_LICENSE = i18n.translate('xpack.apm.correlations.license.text', { + defaultMessage: + 'To use the correlations API, you must be subscribed to an Elastic Platinum license.', +}); + +const fieldCandidatesRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + return withApmSpan( + 'get_correlations_field_candidates', + async () => + await fetchTransactionDurationFieldCandidates(esClient, { + ...resources.params.query, + index: indices.transaction, + }) + ); + }, +}); + +const fieldStatsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/field_stats', + params: t.type({ + body: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldsToSample: t.array(t.string), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldsToSample, ...params } = resources.params.body; + + return withApmSpan( + 'get_correlations_field_stats', + async () => + await fetchFieldsStats( + esClient, + { + ...params, + index: indices.transaction, + }, + fieldsToSample + ) + ); + }, +}); + +const fieldValuePairsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/field_value_pairs', + params: t.type({ + body: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldCandidates: t.array(t.string), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldCandidates, ...params } = resources.params.body; + + return withApmSpan( + 'get_correlations_field_value_pairs', + async () => + await fetchTransactionDurationFieldValuePairs( + esClient, + { + ...params, + index: indices.transaction, + }, + fieldCandidates + ) + ); + }, +}); + +const significantCorrelationsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/significant_correlations', + params: t.type({ + body: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldValuePairs: t.array( + t.type({ + fieldName: t.string, + fieldValue: t.union([t.string, toNumberRt]), + }) + ), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldValuePairs, ...params } = resources.params.body; + + const paramsWithIndex = { + ...params, + index: indices.transaction, + }; + + return withApmSpan( + 'get_significant_correlations', + async () => + await fetchSignificantCorrelations( + esClient, + paramsWithIndex, + fieldValuePairs + ) + ); + }, +}); + +const pValuesRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/p_values', + params: t.type({ + body: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldCandidates: t.array(t.string), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldCandidates, ...params } = resources.params.body; + + const paramsWithIndex = { + ...params, + index: indices.transaction, + }; + + return withApmSpan( + 'get_p_values', + async () => await fetchPValues(esClient, paramsWithIndex, fieldCandidates) + ); + }, +}); + +export const correlationsRouteRepository = createApmServerRouteRepository() + .add(pValuesRoute) + .add(fieldCandidatesRoute) + .add(fieldStatsRoute) + .add(fieldValuePairsRoute) + .add(significantCorrelationsRoute); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index b4b370589e4bc..0c5be4890ba05 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -12,6 +12,7 @@ import type { import { PickByValue } from 'utility-types'; import { alertsChartPreviewRouteRepository } from './alerts/chart_preview'; import { backendsRouteRepository } from './backends'; +import { correlationsRouteRepository } from './correlations'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { environmentsRouteRepository } from './environments'; import { errorsRouteRepository } from './errors'; @@ -60,6 +61,7 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(sourceMapsRouteRepository) .merge(apmFleetRouteRepository) .merge(backendsRouteRepository) + .merge(correlationsRouteRepository) .merge(fallbackToTransactionsRouteRepository) .merge(historicalDataRouteRepository) .merge(eventMetadataRouteRepository); diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts index a20852ef0ae54..22909d5431b4b 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts @@ -7,234 +7,211 @@ import expect from '@kbn/expect'; -import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; - -import type { FailedTransactionsCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/failed_transactions_correlations/types'; -import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; -import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; - import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { parseBfetchResponse } from '../../common/utils/parse_b_fetch'; +import type { FailedTransactionsCorrelationsResponse } from '../../../../plugins/apm/common/correlations/failed_transactions_correlations/types'; +import { EVENT_OUTCOME } from '../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../../plugins/apm/common/event_outcome'; +// These tests go through the full sequence of queries required +// to get the final results for a failed transactions correlation analysis. export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const retry = getService('retry'); - const supertest = getService('legacySupertestAsApmReadUser'); - - const getRequestBody = () => { - const request: IKibanaSearchRequest< - FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams - > = { - params: { - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - percentileThreshold: 95, - }, - }; - - return { - batch: [ - { - request, - options: { strategy: APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS }, - }, - ], - }; - }; + + // This matches the parameters used for the other tab's queries in `../correlations/*`. + const getOptions = () => ({ + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + }); registry.when('failed transactions without data', { config: 'trial', archives: [] }, () => { - it.skip('queries the search strategy and returns results', async () => { - const intialResponse = await supertest - .post(`/internal/bsearch`) - .set('kbn-xsrf', 'foo') - .send(getRequestBody()); + it('handles the empty state', async () => { + const overallDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + }, + }, + }); - expect(intialResponse.status).to.eql( + expect(overallDistributionResponse.status).to.eql( 200, - `Expected status to be '200', got '${intialResponse.status}'` + `Expected status to be '200', got '${overallDistributionResponse.status}'` ); - expect(intialResponse.body).to.eql( - {}, - `Expected response body to be an empty object, actual response is in the text attribute. Got: '${JSON.stringify( - intialResponse.body - )}'` - ); - - const body = parseBfetchResponse(intialResponse)[0]; - - expect(typeof body.result).to.be('object'); - const { result } = body; - - expect(typeof result?.id).to.be('string'); - - // pass on id for follow up queries - const searchStrategyId = result.id; - // follow up request body including search strategy ID - const reqBody = getRequestBody(); - reqBody.batch[0].request.id = searchStrategyId; - - let followUpResponse: Record = {}; - - // continues querying until the search strategy finishes - await retry.waitForWithTimeout( - 'search strategy eventually completes and returns full results', - 5000, - async () => { - const response = await supertest - .post(`/internal/bsearch`) - .set('kbn-xsrf', 'foo') - .send(reqBody); - - followUpResponse = parseBfetchResponse(response)[0]; + const errorDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], + }, + }, + }); - return ( - followUpResponse?.result?.isRunning === false || followUpResponse?.error !== undefined - ); - } + expect(errorDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${errorDistributionResponse.status}'` ); - expect(followUpResponse?.error).to.eql( - undefined, - `search strategy should not return an error, got: ${JSON.stringify( - followUpResponse?.error - )}` - ); + const fieldCandidatesResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + params: { + query: getOptions(), + }, + }); - const followUpResult = followUpResponse.result; - expect(followUpResult?.isRunning).to.eql(false, 'search strategy should not be running'); - expect(followUpResult?.isPartial).to.eql( - false, - 'search strategy result should not be partial' - ); - expect(followUpResult?.id).to.eql( - searchStrategyId, - 'search strategy id should match original id' - ); - expect(followUpResult?.isRestored).to.eql( - true, - 'search strategy response should be restored' + expect(fieldCandidatesResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldCandidatesResponse.status}'` ); - expect(followUpResult?.loaded).to.eql(100, 'loaded state should be 100'); - expect(followUpResult?.total).to.eql(100, 'total state should be 100'); - expect(typeof followUpResult?.rawResponse).to.be('object'); + const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/p_values', + params: { + body: { + ...getOptions(), + fieldCandidates: fieldCandidatesResponse.body?.fieldCandidates, + }, + }, + }); - const { rawResponse: finalRawResponse } = followUpResult; + expect(failedTransactionsCorrelationsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` + ); - expect(typeof finalRawResponse?.took).to.be('number'); + const finalRawResponse: FailedTransactionsCorrelationsResponse = { + ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, + percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, + overallHistogram: overallDistributionResponse.body?.overallHistogram, + failedTransactionsCorrelations: + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, + }; - expect(finalRawResponse?.failedTransactionsCorrelations.length).to.eql( + expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( 0, - `Expected 0 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations.length}.` + `Expected 0 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` ); }); }); registry.when('failed transactions with data', { config: 'trial', archives: ['8.0.0'] }, () => { - it.skip('queries the search strategy and returns results', async () => { - const intialResponse = await supertest - .post(`/internal/bsearch`) - .set('kbn-xsrf', 'foo') - .send(getRequestBody()); + it('runs queries and returns results', async () => { + const overallDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + }, + }, + }); - expect(intialResponse.status).to.eql( + expect(overallDistributionResponse.status).to.eql( 200, - `Expected status to be '200', got '${intialResponse.status}'` + `Expected status to be '200', got '${overallDistributionResponse.status}'` ); - expect(intialResponse.body).to.eql( - {}, - `Expected response body to be an empty object, actual response is in the text attribute. Got: '${JSON.stringify( - intialResponse.body - )}'` - ); - - const body = parseBfetchResponse(intialResponse)[0]; - - expect(typeof body.result).to.be('object'); - const { result } = body; - expect(typeof result?.id).to.be('string'); - - // pass on id for follow up queries - const searchStrategyId = result.id; - - // follow up request body including search strategy ID - const reqBody = getRequestBody(); - reqBody.batch[0].request.id = searchStrategyId; - - let followUpResponse: Record = {}; + const errorDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], + }, + }, + }); - // continues querying until the search strategy finishes - await retry.waitForWithTimeout( - 'search strategy eventually completes and returns full results', - 5000, - async () => { - const response = await supertest - .post(`/internal/bsearch`) - .set('kbn-xsrf', 'foo') - .send(reqBody); + expect(errorDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${errorDistributionResponse.status}'` + ); - followUpResponse = parseBfetchResponse(response)[0]; + const fieldCandidatesResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + params: { + query: getOptions(), + }, + }); - return ( - followUpResponse?.result?.isRunning === false || followUpResponse?.error !== undefined - ); - } + expect(fieldCandidatesResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldCandidatesResponse.status}'` ); - expect(followUpResponse?.error).to.eql( - undefined, - `search strategy should not return an error, got: ${JSON.stringify( - followUpResponse?.error - )}` + const fieldCandidates = fieldCandidatesResponse.body?.fieldCandidates.filter( + (t) => !(t === EVENT_OUTCOME) ); - const followUpResult = followUpResponse.result; - expect(followUpResult?.isRunning).to.eql(false, 'search strategy should not be running'); - expect(followUpResult?.isPartial).to.eql( - false, - 'search strategy result should not be partial' + // Identified 68 fieldCandidates. + expect(fieldCandidates.length).to.eql( + 68, + `Expected field candidates length to be '68', got '${fieldCandidates.length}'` ); - expect(followUpResult?.id).to.eql( - searchStrategyId, - 'search strategy id should match original id' - ); - expect(followUpResult?.isRestored).to.eql( - true, - 'search strategy response should be restored' - ); - expect(followUpResult?.loaded).to.eql(100, 'loaded state should be 100'); - expect(followUpResult?.total).to.eql(100, 'total state should be 100'); - expect(typeof followUpResult?.rawResponse).to.be('object'); + const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/p_values', + params: { + body: { + ...getOptions(), + fieldCandidates, + }, + }, + }); - const { rawResponse: finalRawResponse } = followUpResult; + expect(failedTransactionsCorrelationsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` + ); + + const fieldsToSample = new Set(); + if (failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.length > 0) { + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.forEach((d) => { + fieldsToSample.add(d.fieldName); + }); + } + + const failedtransactionsFieldStats = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/field_stats', + params: { + body: { + ...getOptions(), + fieldsToSample: [...fieldsToSample], + }, + }, + }); + + const finalRawResponse: FailedTransactionsCorrelationsResponse = { + ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, + percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, + overallHistogram: overallDistributionResponse.body?.overallHistogram, + errorHistogram: errorDistributionResponse.body?.overallHistogram, + failedTransactionsCorrelations: + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, + fieldStats: failedtransactionsFieldStats.body?.stats, + }; - expect(typeof finalRawResponse?.took).to.be('number'); expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); - expect(finalRawResponse?.errorHistogram.length).to.be(101); - expect(finalRawResponse?.overallHistogram.length).to.be(101); - expect(finalRawResponse?.fieldStats.length).to.be(26); + expect(finalRawResponse?.errorHistogram?.length).to.be(101); + expect(finalRawResponse?.overallHistogram?.length).to.be(101); + expect(finalRawResponse?.fieldStats?.length).to.be(26); - expect(finalRawResponse?.failedTransactionsCorrelations.length).to.eql( + expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( 30, - `Expected 30 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations.length}.` + `Expected 30 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` ); - expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ - 'Fetched 95th percentile value of 1309695.875 based on 1244 documents.', - 'Identified 68 fieldCandidates.', - 'Identified correlations for 68 fields out of 68 candidates.', - 'Identified 26 fields to sample for field statistics.', - 'Retrieved field statistics for 26 fields out of 26 fields.', - 'Identified 30 significant correlations relating to failed transactions.', - ]); - - const sortedCorrelations = finalRawResponse?.failedTransactionsCorrelations.sort(); - const correlation = sortedCorrelations[0]; + const sortedCorrelations = finalRawResponse?.failedTransactionsCorrelations?.sort( + (a, b) => b.score - a.score + ); + const correlation = sortedCorrelations?.[0]; expect(typeof correlation).to.be('object'); expect(correlation?.doc_count).to.be(31); @@ -247,10 +224,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(typeof correlation?.failurePercentage).to.be('number'); expect(typeof correlation?.successPercentage).to.be('number'); - const fieldStats = finalRawResponse?.fieldStats[0]; + const fieldStats = finalRawResponse?.fieldStats?.[0]; expect(typeof fieldStats).to.be('object'); - expect(fieldStats.topValues.length).to.greaterThan(0); - expect(fieldStats.topValuesSampleSize).to.greaterThan(0); + expect(Array.isArray(fieldStats?.topValues) && fieldStats?.topValues?.length).to.greaterThan( + 0 + ); + expect(fieldStats?.topValuesSampleSize).to.greaterThan(0); }); }); } diff --git a/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts new file mode 100644 index 0000000000000..a62145da25326 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const registry = getService('registry'); + + const endpoint = 'GET /internal/apm/correlations/field_candidates'; + + const getOptions = () => ({ + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + }, + }, + }); + + registry.when('field candidates without data', { config: 'trial', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + expect(response.body?.fieldCandidates.length).to.be(14); + }); + }); + + registry.when( + 'field candidates with data and default args', + { config: 'trial', archives: ['8.0.0'] }, + () => { + it('returns field candidates', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + expect(response.body?.fieldCandidates.length).to.be(69); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts new file mode 100644 index 0000000000000..df9314546d6de --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const registry = getService('registry'); + + const endpoint = 'POST /internal/apm/correlations/field_value_pairs'; + + const getOptions = () => ({ + params: { + body: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + fieldCandidates: [ + 'service.version', + 'service.node.name', + 'service.framework.version', + 'service.language.version', + 'service.runtime.version', + 'kubernetes.pod.name', + 'kubernetes.pod.uid', + 'container.id', + 'source.ip', + 'client.ip', + 'host.ip', + 'service.environment', + 'process.args', + 'http.response.status_code', + ], + }, + }, + }); + + registry.when('field value pairs without data', { config: 'trial', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + expect(response.body?.fieldValuePairs.length).to.be(0); + }); + }); + + registry.when( + 'field value pairs with data and default args', + { config: 'trial', archives: ['8.0.0'] }, + () => { + it('returns field value pairs', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + expect(response.body?.fieldValuePairs.length).to.be(124); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts index 8d768f559fb6d..5d73a6a0499b0 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts @@ -7,134 +7,95 @@ import expect from '@kbn/expect'; -import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; - -import type { LatencyCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/latency_correlations/types'; -import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; -import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; - import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { parseBfetchResponse } from '../../common/utils/parse_b_fetch'; +import type { LatencyCorrelationsResponse } from '../../../../plugins/apm/common/correlations/latency_correlations/types'; +// These tests go through the full sequence of queries required +// to get the final results for a latency correlation analysis. export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const retry = getService('retry'); - const supertest = getService('legacySupertestAsApmReadUser'); - - const getRequestBody = () => { - const request: IKibanaSearchRequest = - { - params: { - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - percentileThreshold: 95, - analyzeCorrelations: true, - }, - }; - - return { - batch: [ - { - request, - options: { strategy: APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS }, - }, - ], - }; - }; + + // This matches the parameters used for the other tab's queries in `../correlations/*`. + const getOptions = () => ({ + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + }); registry.when( - 'correlations latency_ml overall without data', + 'correlations latency overall without data', { config: 'trial', archives: [] }, () => { it('handles the empty state', async () => { - const intialResponse = await supertest - .post(`/internal/bsearch`) - .set('kbn-xsrf', 'foo') - .send(getRequestBody()); - - expect(intialResponse.status).to.eql( + const overallDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + }, + }, + }); + + expect(overallDistributionResponse.status).to.eql( 200, - `Expected status to be '200', got '${intialResponse.status}'` + `Expected status to be '200', got '${overallDistributionResponse.status}'` ); - expect(intialResponse.body).to.eql( - {}, - `Expected response body to be an empty object, actual response is in the text attribute. Got: '${JSON.stringify( - intialResponse.body - )}'` - ); - - const body = parseBfetchResponse(intialResponse)[0]; - - expect(typeof body.result).to.be('object'); - const { result } = body; - - expect(typeof result?.id).to.be('string'); - // pass on id for follow up queries - const searchStrategyId = result.id; + const fieldCandidatesResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + params: { + query: getOptions(), + }, + }); - // follow up request body including search strategy ID - const reqBody = getRequestBody(); - reqBody.batch[0].request.id = searchStrategyId; - - let followUpResponse: Record = {}; - - // continues querying until the search strategy finishes - await retry.waitForWithTimeout( - 'search strategy eventually completes and returns full results', - 5000, - async () => { - const response = await supertest - .post(`/internal/bsearch`) - .set('kbn-xsrf', 'foo') - .send(reqBody); - - followUpResponse = parseBfetchResponse(response)[0]; - - return ( - followUpResponse?.result?.isRunning === false || followUpResponse?.error !== undefined - ); - } + expect(fieldCandidatesResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldCandidatesResponse.status}'` ); - expect(followUpResponse?.error).to.eql( - undefined, - `search strategy should not return an error, got: ${JSON.stringify( - followUpResponse?.error - )}` + const fieldValuePairsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/field_value_pairs', + params: { + body: { + ...getOptions(), + fieldCandidates: fieldCandidatesResponse.body?.fieldCandidates, + }, + }, + }); + + expect(fieldValuePairsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldValuePairsResponse.status}'` ); - const followUpResult = followUpResponse.result; - expect(followUpResult?.isRunning).to.eql(false, 'search strategy should not be running'); - expect(followUpResult?.isPartial).to.eql( - false, - 'search strategy result should not be partial' - ); - expect(followUpResult?.id).to.eql( - searchStrategyId, - 'search strategy id should match original id' - ); - expect(followUpResult?.isRestored).to.eql( - true, - 'search strategy response should be restored' + const significantCorrelationsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/significant_correlations', + params: { + body: { + ...getOptions(), + fieldValuePairs: fieldValuePairsResponse.body?.fieldValuePairs, + }, + }, + }); + + expect(significantCorrelationsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${significantCorrelationsResponse.status}'` ); - expect(followUpResult?.loaded).to.eql(100, 'loaded state should be 100'); - expect(followUpResult?.total).to.eql(100, 'total state should be 100'); - expect(typeof followUpResult?.rawResponse).to.be('object'); + const finalRawResponse: LatencyCorrelationsResponse = { + ccsWarning: significantCorrelationsResponse.body?.ccsWarning, + percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, + overallHistogram: overallDistributionResponse.body?.overallHistogram, + latencyCorrelations: significantCorrelationsResponse.body?.latencyCorrelations, + }; - const { rawResponse: finalRawResponse } = followUpResult; - - expect(typeof finalRawResponse?.took).to.be('number'); expect(finalRawResponse?.percentileThresholdValue).to.be(undefined); expect(finalRawResponse?.overallHistogram).to.be(undefined); - expect(finalRawResponse?.latencyCorrelations.length).to.be(0); - expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ - 'Fetched 95th percentile value of undefined based on 0 documents.', - 'Abort service since percentileThresholdValue could not be determined.', - ]); + expect(finalRawResponse?.latencyCorrelations?.length).to.be(0); }); } ); @@ -144,120 +105,121 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'trial', archives: ['8.0.0'] }, () => { // putting this into a single `it` because the responses depend on each other - it.skip('queries the search strategy and returns results', async () => { - const intialResponse = await supertest - .post(`/internal/bsearch`) - .set('kbn-xsrf', 'foo') - .send(getRequestBody()); - - expect(intialResponse.status).to.eql( + it('runs queries and returns results', async () => { + const overallDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + }, + }, + }); + + expect(overallDistributionResponse.status).to.eql( 200, - `Expected status to be '200', got '${intialResponse.status}'` + `Expected status to be '200', got '${overallDistributionResponse.status}'` ); - expect(intialResponse.body).to.eql( - {}, - `Expected response body to be an empty object, actual response is in the text attribute. Got: '${JSON.stringify( - intialResponse.body - )}'` - ); - - const body = parseBfetchResponse(intialResponse)[0]; - - expect(typeof body?.result).to.be('object'); - const { result } = body; - - expect(typeof result?.id).to.be('string'); - // pass on id for follow up queries - const searchStrategyId = result.id; + const fieldCandidatesResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/correlations/field_candidates', + params: { + query: getOptions(), + }, + }); - expect(result?.loaded).to.be(0); - expect(result?.total).to.be(100); - expect(result?.isRunning).to.be(true); - expect(result?.isPartial).to.be(true); - expect(result?.isRestored).to.eql( - false, - `Expected response result to be not restored. Got: '${result?.isRestored}'` - ); - expect(typeof result?.rawResponse).to.be('object'); - - const { rawResponse } = result; - - expect(typeof rawResponse?.took).to.be('number'); - expect(rawResponse?.latencyCorrelations).to.eql([]); - - // follow up request body including search strategy ID - const reqBody = getRequestBody(); - reqBody.batch[0].request.id = searchStrategyId; - - let followUpResponse: Record = {}; - - // continues querying until the search strategy finishes - await retry.waitForWithTimeout( - 'search strategy eventually completes and returns full results', - 5000, - async () => { - const response = await supertest - .post(`/internal/bsearch`) - .set('kbn-xsrf', 'foo') - .send(reqBody); - followUpResponse = parseBfetchResponse(response)[0]; - - return ( - followUpResponse?.result?.isRunning === false || followUpResponse?.error !== undefined - ); - } + expect(fieldCandidatesResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldCandidatesResponse.status}'` ); - expect(followUpResponse?.error).to.eql( - undefined, - `Finished search strategy should not return an error, got: ${JSON.stringify( - followUpResponse?.error - )}` + // Identified 69 fieldCandidates. + expect(fieldCandidatesResponse.body?.fieldCandidates.length).to.eql( + 69, + `Expected field candidates length to be '69', got '${fieldCandidatesResponse.body?.fieldCandidates.length}'` ); - const followUpResult = followUpResponse.result; - expect(followUpResult?.isRunning).to.eql( - false, - `Expected finished result not to be running. Got: ${followUpResult?.isRunning}` + const fieldValuePairsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/field_value_pairs', + params: { + body: { + ...getOptions(), + fieldCandidates: fieldCandidatesResponse.body?.fieldCandidates, + }, + }, + }); + + expect(fieldValuePairsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldValuePairsResponse.status}'` ); - expect(followUpResult?.isPartial).to.eql( - false, - `Expected finished result not to be partial. Got: ${followUpResult?.isPartial}` + + // Identified 379 fieldValuePairs. + expect(fieldValuePairsResponse.body?.fieldValuePairs.length).to.eql( + 379, + `Expected field value pairs length to be '379', got '${fieldValuePairsResponse.body?.fieldValuePairs.length}'` ); - expect(followUpResult?.id).to.be(searchStrategyId); - expect(followUpResult?.isRestored).to.be(true); - expect(followUpResult?.loaded).to.be(100); - expect(followUpResult?.total).to.be(100); - expect(typeof followUpResult?.rawResponse).to.be('object'); + const significantCorrelationsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/significant_correlations', + params: { + body: { + ...getOptions(), + fieldValuePairs: fieldValuePairsResponse.body?.fieldValuePairs, + }, + }, + }); + + expect(significantCorrelationsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${significantCorrelationsResponse.status}'` + ); - const { rawResponse: finalRawResponse } = followUpResult; + // Loaded fractions and totalDocCount of 1244. + expect(significantCorrelationsResponse.body?.totalDocCount).to.eql( + 1244, + `Expected 1244 total doc count, got ${significantCorrelationsResponse.body?.totalDocCount}.` + ); - expect(typeof finalRawResponse?.took).to.be('number'); + const fieldsToSample = new Set(); + if (significantCorrelationsResponse.body?.latencyCorrelations.length > 0) { + significantCorrelationsResponse.body?.latencyCorrelations.forEach((d) => { + fieldsToSample.add(d.fieldName); + }); + } + + const failedtransactionsFieldStats = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/field_stats', + params: { + body: { + ...getOptions(), + fieldsToSample: [...fieldsToSample], + }, + }, + }); + + const finalRawResponse: LatencyCorrelationsResponse = { + ccsWarning: significantCorrelationsResponse.body?.ccsWarning, + percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, + overallHistogram: overallDistributionResponse.body?.overallHistogram, + latencyCorrelations: significantCorrelationsResponse.body?.latencyCorrelations, + fieldStats: failedtransactionsFieldStats.body?.stats, + }; + + // Fetched 95th percentile value of 1309695.875 based on 1244 documents. expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); - expect(finalRawResponse?.overallHistogram.length).to.be(101); - expect(finalRawResponse?.fieldStats.length).to.be(12); + expect(finalRawResponse?.overallHistogram?.length).to.be(101); + expect(finalRawResponse?.fieldStats?.length).to.be(12); - expect(finalRawResponse?.latencyCorrelations.length).to.eql( + // Identified 13 significant correlations out of 379 field/value pairs. + expect(finalRawResponse?.latencyCorrelations?.length).to.eql( 13, - `Expected 13 identified correlations, got ${finalRawResponse?.latencyCorrelations.length}.` + `Expected 13 identified correlations, got ${finalRawResponse?.latencyCorrelations?.length}.` ); - expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ - 'Fetched 95th percentile value of 1309695.875 based on 1244 documents.', - 'Loaded histogram range steps.', - 'Loaded overall histogram chart data.', - 'Loaded percentiles.', - 'Identified 69 fieldCandidates.', - 'Identified 379 fieldValuePairs.', - 'Loaded fractions and totalDocCount of 1244.', - 'Identified 13 significant correlations out of 379 field/value pairs.', - 'Identified 12 fields to sample for field statistics.', - 'Retrieved field statistics for 12 fields out of 12 fields.', - ]); - - const correlation = finalRawResponse?.latencyCorrelations[0]; + const correlation = finalRawResponse?.latencyCorrelations?.sort( + (a, b) => b.correlation - a.correlation + )[0]; expect(typeof correlation).to.be('object'); expect(correlation?.fieldName).to.be('transaction.result'); expect(correlation?.fieldValue).to.be('success'); @@ -265,10 +227,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(correlation?.ksTest).to.be(4.806503252860024e-13); expect(correlation?.histogram.length).to.be(101); - const fieldStats = finalRawResponse?.fieldStats[0]; + const fieldStats = finalRawResponse?.fieldStats?.[0]; expect(typeof fieldStats).to.be('object'); - expect(fieldStats.topValues.length).to.greaterThan(0); - expect(fieldStats.topValuesSampleSize).to.greaterThan(0); + expect( + Array.isArray(fieldStats?.topValues) && fieldStats?.topValues?.length + ).to.greaterThan(0); + expect(fieldStats?.topValuesSampleSize).to.greaterThan(0); }); } ); diff --git a/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts new file mode 100644 index 0000000000000..1f3dd58063087 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const registry = getService('registry'); + + const endpoint = 'POST /internal/apm/correlations/p_values'; + + const getOptions = () => ({ + params: { + body: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + fieldCandidates: [ + 'service.version', + 'service.node.name', + 'service.framework.version', + 'service.language.version', + 'service.runtime.version', + 'kubernetes.pod.name', + 'kubernetes.pod.uid', + 'container.id', + 'source.ip', + 'client.ip', + 'host.ip', + 'service.environment', + 'process.args', + 'http.response.status_code', + ], + }, + }, + }); + + registry.when('p values without data', { config: 'trial', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + expect(response.body?.failedTransactionsCorrelations.length).to.be(0); + }); + }); + + registry.when( + 'p values with data and default args', + { config: 'trial', archives: ['8.0.0'] }, + () => { + it('returns p values', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + expect(response.body?.failedTransactionsCorrelations.length).to.be(15); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts new file mode 100644 index 0000000000000..994f23bbf2a4e --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const registry = getService('registry'); + + const endpoint = 'POST /internal/apm/correlations/significant_correlations'; + + const getOptions = () => ({ + params: { + body: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + fieldValuePairs: [ + { fieldName: 'service.version', fieldValue: '2020-08-26 02:09:20' }, + { fieldName: 'service.version', fieldValue: 'None' }, + { + fieldName: 'service.node.name', + fieldValue: 'af586da824b28435f3a8c8f0c016096502cd2495d64fb332db23312be88cfff6', + }, + { + fieldName: 'service.node.name', + fieldValue: 'asdf', + }, + { fieldName: 'service.runtime.version', fieldValue: '12.18.3' }, + { fieldName: 'service.runtime.version', fieldValue: '2.6.6' }, + { + fieldName: 'kubernetes.pod.name', + fieldValue: 'opbeans-node-6cf6cf6f58-r5q9l', + }, + { + fieldName: 'kubernetes.pod.name', + fieldValue: 'opbeans-java-6dc7465984-h9sh5', + }, + { + fieldName: 'kubernetes.pod.uid', + fieldValue: '8da9c944-e741-11ea-819e-42010a84004a', + }, + { + fieldName: 'kubernetes.pod.uid', + fieldValue: '8e192c6c-e741-11ea-819e-42010a84004a', + }, + { + fieldName: 'container.id', + fieldValue: 'af586da824b28435f3a8c8f0c016096502cd2495d64fb332db23312be88cfff6', + }, + { + fieldName: 'container.id', + fieldValue: 'asdf', + }, + { fieldName: 'host.ip', fieldValue: '10.52.6.48' }, + { fieldName: 'host.ip', fieldValue: '10.52.6.50' }, + ], + }, + }, + }); + + registry.when('significant correlations without data', { config: 'trial', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + expect(response.body?.latencyCorrelations.length).to.be(0); + }); + }); + + registry.when( + 'significant correlations with data and default args', + { config: 'trial', archives: ['8.0.0'] }, + () => { + it('returns significant correlations', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + expect(response.body?.latencyCorrelations.length).to.be(7); + }); + } + ); +} From 90df011d9beceedbb1c8f610b7c8b8a561d1e810 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Tue, 9 Nov 2021 13:03:01 +0300 Subject: [PATCH 22/98] [TSVB] Fix reappearing of hidden series on refresh and styles loading (#117311) * [TSVB] Fix reappearing of hidden series on refresh and styles loading * Add functional test * Update condition and move loading component to another file Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/timeseries_loading.tsx | 16 +++ .../components/timeseries_visualization.tsx | 110 +++++++++--------- .../public/timeseries_vis_renderer.tsx | 74 +++++------- .../apps/visualize/_tsvb_time_series.ts | 34 ++++++ .../page_objects/visual_builder_page.ts | 4 + 5 files changed, 139 insertions(+), 99 deletions(-) create mode 100644 src/plugins/vis_types/timeseries/public/application/components/timeseries_loading.tsx diff --git a/src/plugins/vis_types/timeseries/public/application/components/timeseries_loading.tsx b/src/plugins/vis_types/timeseries/public/application/components/timeseries_loading.tsx new file mode 100644 index 0000000000000..ae0088d22cf76 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/timeseries_loading.tsx @@ -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 React from 'react'; +import { EuiLoadingChart } from '@elastic/eui'; + +export const TimeseriesLoading = () => ( +
+ +
+); diff --git a/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx index 0916892cfda46..ae699880784a9 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/timeseries_visualization.tsx @@ -8,7 +8,7 @@ import './timeseries_visualization.scss'; -import React, { Suspense, useCallback, useEffect } from 'react'; +import React, { Suspense, useCallback, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart } from '@elastic/eui'; import { XYChartSeriesIdentifier, GeometryValue } from '@elastic/charts'; import { IUiSettingsClient } from 'src/core/public'; @@ -16,8 +16,9 @@ import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { PersistedState } from 'src/plugins/visualizations/public'; import { PaletteRegistry } from 'src/plugins/charts/public'; +import { TimeseriesLoading } from './timeseries_loading'; import { TimeseriesVisTypes } from './vis_types'; -import type { PanelData, TimeseriesVisData } from '../../../common/types'; +import type { FetchedIndexPattern, PanelData, TimeseriesVisData } from '../../../common/types'; import { isVisTableData } from '../../../common/vis_data_utils'; import { TimeseriesVisParams } from '../../types'; import { convertSeriesToDataTable } from './lib/convert_series_to_datatable'; @@ -27,32 +28,41 @@ import { LastValueModeIndicator } from './last_value_mode_indicator'; import { getInterval } from './lib/get_interval'; import { AUTO_INTERVAL } from '../../../common/constants'; import { TIME_RANGE_DATA_MODES, PANEL_TYPES } from '../../../common/enums'; -import type { IndexPattern } from '../../../../../data/common'; -import '../index.scss'; +import { fetchIndexPattern } from '../../../common/index_patterns_utils'; +import { getCharts, getDataStart } from '../../services'; interface TimeseriesVisualizationProps { - className?: string; getConfig: IUiSettingsClient['get']; handlers: IInterpreterRenderHandlers; model: TimeseriesVisParams; visData: TimeseriesVisData; uiState: PersistedState; syncColors: boolean; - palettesService: PaletteRegistry; - indexPattern?: IndexPattern | null; } function TimeseriesVisualization({ - className = 'tvbVis', visData, model, handlers, uiState, getConfig, syncColors, - palettesService, - indexPattern, }: TimeseriesVisualizationProps) { + const [indexPattern, setIndexPattern] = useState(null); + const [palettesService, setPalettesService] = useState(null); + + useEffect(() => { + getCharts() + .palettes.getPalettes() + .then((paletteRegistry) => setPalettesService(paletteRegistry)); + }, []); + + useEffect(() => { + fetchIndexPattern(model.index_pattern, getDataStart().indexPatterns).then( + (fetchedIndexPattern) => setIndexPattern(fetchedIndexPattern.indexPattern) + ); + }, [model.index_pattern]); + const onBrush = useCallback( async (gte: string, lte: string, series: PanelData[]) => { let event; @@ -136,10 +146,6 @@ function TimeseriesVisualization({ [uiState] ); - useEffect(() => { - handlers.done(); - }); - const VisComponent = TimeseriesVisTypes[model.type]; const isLastValueMode = @@ -150,46 +156,46 @@ function TimeseriesVisualization({ const [firstSeries] = (isVisTableData(visData) ? visData.series : visData[model.id]?.series) ?? []; - if (VisComponent) { - return ( - - {shouldDisplayLastValueIndicator && ( - - - - )} - - - - - } - > - - - - - ); + if (!VisComponent || palettesService === null || indexPattern === null) { + return ; } - return
; + return ( + + {shouldDisplayLastValueIndicator && ( + + + + )} + + + +
+ } + > + + + + + ); } // default export required for React.Lazy diff --git a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx index ad069a4d7e2cc..9edc05893e24f 100644 --- a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx @@ -13,13 +13,10 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient } from 'kibana/public'; -import { EuiLoadingChart } from '@elastic/eui'; -import { fetchIndexPattern } from '../common/index_patterns_utils'; import { VisualizationContainer, PersistedState } from '../../../visualizations/public'; import type { TimeseriesVisData } from '../common/types'; import { isVisTableData } from '../common/vis_data_utils'; -import { getCharts, getDataStart } from './services'; import type { TimeseriesVisParams } from './types'; import type { ExpressionRenderDefinition } from '../../../expressions/common'; @@ -44,57 +41,40 @@ export const getTimeseriesVisRenderer: (deps: { name: 'timeseries_vis', reuseDomNode: true, render: async (domNode, config, handlers) => { + // Build optimization. Move app styles from main bundle + // @ts-expect-error TS error, cannot find type declaration for scss + import('./application/index.scss'); + handlers.onDestroy(() => { unmountComponentAtNode(domNode); }); const { visParams: model, visData, syncColors } = config; - const { palettes } = getCharts(); - const { indexPatterns } = getDataStart(); const showNoResult = !checkIfDataExists(visData, model); - let servicesLoaded; - - Promise.all([ - palettes.getPalettes(), - fetchIndexPattern(model.index_pattern, indexPatterns), - ]).then(([palettesService, { indexPattern }]) => { - servicesLoaded = true; - - unmountComponentAtNode(domNode); - - render( - - + + - - - , - domNode - ); - }); - - if (!servicesLoaded) { - render( -
- -
, - domNode - ); - } + model={model} + visData={visData as TimeseriesVisData} + syncColors={syncColors} + uiState={handlers.uiState! as PersistedState} + /> + + , + domNode, + () => { + handlers.done(); + } + ); }, }); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 009e4a07cd42a..69cc764c39b21 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -361,6 +361,40 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(chartData).to.eql(expectedChartData); }); + describe('Hiding series', () => { + it('should hide series by legend item click', async () => { + await visualBuilder.clickDataTab('timeSeries'); + await visualBuilder.setMetricsGroupByTerms('@tags.raw'); + + let areasCount = (await visualBuilder.getChartItems())?.length; + expect(areasCount).to.be(6); + + await visualBuilder.clickSeriesLegendItem('success'); + await visualBuilder.clickSeriesLegendItem('info'); + await visualBuilder.clickSeriesLegendItem('error'); + + areasCount = (await visualBuilder.getChartItems())?.length; + expect(areasCount).to.be(3); + }); + + it('should keep series hidden after refresh', async () => { + await visualBuilder.clickDataTab('timeSeries'); + await visualBuilder.setMetricsGroupByTerms('extension.raw'); + + let legendNames = await visualBuilder.getLegendNames(); + expect(legendNames).to.eql(['jpg', 'css', 'png', 'gif', 'php']); + + await visualBuilder.clickSeriesLegendItem('png'); + await visualBuilder.clickSeriesLegendItem('php'); + legendNames = await visualBuilder.getLegendNames(); + expect(legendNames).to.eql(['jpg', 'css', 'gif']); + + await visualize.clickRefresh(true); + legendNames = await visualBuilder.getLegendNames(); + expect(legendNames).to.eql(['jpg', 'css', 'gif']); + }); + }); + describe('Query filter', () => { it('should display correct chart data for applied series filter', async () => { const expectedChartData = [ diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index b87962b34291c..082bee1f973fa 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -878,6 +878,10 @@ export class VisualBuilderPageObject extends FtrService { await optionInput.type(query); } + public async clickSeriesLegendItem(name: string) { + await this.find.clickByCssSelector(`[data-ech-series-name="${name}"] .echLegendItem__label`); + } + public async toggleNewChartsLibraryWithDebug(enabled: boolean) { await this.elasticChart.setNewChartUiDebugFlag(enabled); } From 8978fd27f6ae42fe851116bdc3b2dceef75a9b0d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 9 Nov 2021 11:29:53 +0100 Subject: [PATCH 23/98] [ML] Update headers structure for the API docs (#117865) * change headers level for new docs * increase level for groups and methods, remove h2 for the project name * bump version --- x-pack/plugins/ml/server/routes/apidoc.json | 2 +- .../server/routes/apidoc_scripts/template.md | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 77e5443d0a257..b7bd92c913891 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -1,6 +1,6 @@ { "name": "ml_kibana_api", - "version": "7.15.0", + "version": "8.0.0", "description": "This is the documentation of the REST API provided by the Machine Learning Kibana plugin. Each API is experimental and can include breaking changes in any version.", "title": "ML Kibana API", "order": [ diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md b/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md index 72104e3e433da..11a469bfeec5d 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md @@ -3,7 +3,7 @@ <% } -%> -# <%= project.name %> v<%= project.version %> +v<%= project.version %> <%= project.description %> @@ -24,7 +24,7 @@ ``` <% if (sub.header && sub.header.fields && sub.header.fields.Header.length) { -%> -#### Headers +##### Headers | Name | Type | Description | |---------|-----------|--------------------------------------| <% sub.header.fields.Header.forEach(header => { -%> @@ -33,7 +33,7 @@ <% } // if parameters -%> <% if (sub.header && sub.header.examples && sub.header.examples.length) { -%> -#### Header examples +##### Header examples <% sub.header.examples.forEach(example => { -%> <%= example.title %> @@ -45,7 +45,7 @@ <% if (sub.parameter && sub.parameter.fields) { -%> <% Object.keys(sub.parameter.fields).forEach(g => { -%> -#### Parameters - `<%= g -%>` +##### Parameters - `<%= g -%>` | Name | Type | Description | |:---------|:-----------|:--------------------------------------| <% sub.parameter.fields[g].forEach(param => { -%> @@ -61,7 +61,7 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if parameters -%> <% if (sub.examples && sub.examples.length) { -%> -#### Examples +##### Examples <% sub.examples.forEach(example => { -%> <%= example.title %> @@ -72,7 +72,7 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if example -%> <% if (sub.parameter && sub.parameter.examples && sub.parameter.examples.length) { -%> -#### Parameters examples +##### Parameters examples <% sub.parameter.examples.forEach(exampleParam => { -%> `<%= exampleParam.type %>` - <%= exampleParam.title %> @@ -83,10 +83,10 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if exampleParam -%> <% if (sub.success && sub.success.fields) { -%> -#### Success response +##### Success response <% Object.keys(sub.success.fields).forEach(g => { -%> -##### Success response - `<%= g %>` +###### Success response - `<%= g %>` | Name | Type | Description | |:---------|:-----------|:--------------------------------------| <% sub.success.fields[g].forEach(param => { -%> @@ -102,10 +102,10 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if success.fields -%> <% if (sub.success && sub.success.examples && sub.success.examples.length) { -%> -#### Success response example +##### Success response example <% sub.success.examples.forEach(example => { -%> -##### Success response example - `<%= example.title %>` +###### Success response example - `<%= example.title %>` ``` <%- example.content %> @@ -114,10 +114,10 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if success.examples -%> <% if (sub.error && sub.error.fields) { -%> -#### Error response +##### Error response <% Object.keys(sub.error.fields).forEach(g => { -%> -##### Error response - `<%= g %>` +###### Error response - `<%= g %>` | Name | Type | Description | |:---------|:-----------|:--------------------------------------| <% sub.error.fields[g].forEach(param => { -%> @@ -133,10 +133,10 @@ _Allowed values: <%- param.allowedValues %>_<% } -%> | <% } // if error.fields -%> <% if (sub.error && sub.error.examples && sub.error.examples.length) { -%> -#### Error response example +##### Error response example <% sub.error.examples.forEach(example => { -%> -##### Error response example - `<%= example.title %>` +###### Error response example - `<%= example.title %>` ``` <%- example.content %> From a43fd6507cdee277809af0312c0d9efb57efc80b Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 9 Nov 2021 11:39:30 +0100 Subject: [PATCH 24/98] Always call `forceLogout` first in the test cleanup code. (#117555) --- x-pack/test/accessibility/apps/login_page.ts | 1 + x-pack/test/accessibility/apps/ml.ts | 1 + .../apps/ml_embeddables_in_dashboard.ts | 4 +++- x-pack/test/accessibility/apps/transform.ts | 1 + .../advanced_settings_security.ts | 1 + .../apps/apm/feature_controls/apm_security.ts | 1 + .../feature_controls/canvas_security.ts | 1 + .../feature_controls/dashboard_security.ts | 7 +++--- .../time_to_visualize_security.ts | 7 +++--- .../feature_controls/dev_tools_security.ts | 1 + .../feature_controls/discover_security.ts | 7 +++--- .../graph/feature_controls/graph_security.ts | 1 + .../home/feature_controls/home_security.ts | 7 +++--- .../index_patterns_security.ts | 1 + .../infrastructure_security.ts | 2 ++ .../infra/feature_controls/logs_security.ts | 3 +++ .../maps/feature_controls/maps_security.ts | 2 ++ .../apps/maps/feature_controls/maps_spaces.ts | 1 + .../apps/ml/feature_controls/ml_security.ts | 5 ++-- x-pack/test/functional/apps/ml/index.ts | 8 +++++-- .../apps/ml/permissions/full_ml_access.ts | 2 ++ .../apps/ml/permissions/no_ml_access.ts | 1 + .../apps/ml/permissions/read_ml_access.ts | 2 ++ .../feature_controls/monitoring_security.ts | 7 +++--- .../feature_controls/monitoring_spaces.ts | 4 ++-- .../saved_objects_management_security.ts | 3 +++ ...y_roles.js => doc_level_security_roles.ts} | 9 ++++--- ...el_security.js => field_level_security.ts} | 12 ++++------ .../apps/security/{index.js => index.ts} | 4 +++- .../security/{management.js => management.ts} | 9 ++++--- ...ure_roles_perm.js => secure_roles_perm.ts} | 7 +++--- .../test/functional/apps/security/security.ts | 1 + .../security/{user_email.js => user_email.ts} | 4 +++- .../functional/apps/spaces/enter_space.ts | 1 + .../feature_controls/spaces_security.ts | 10 +++++--- .../apps/spaces/spaces_selection.ts | 1 + .../test/functional/apps/transform/index.ts | 4 +++- .../permissions/full_transform_access.ts | 1 + .../permissions/read_transform_access.ts | 1 + .../feature_controls/uptime_security.ts | 1 + .../feature_controls/visualize_security.ts | 8 ++++++- .../functional/page_objects/security_page.ts | 24 ++++++++----------- .../apps/ml/permissions/full_ml_access.ts | 1 + .../apps/ml/permissions/no_ml_access.ts | 1 + .../apps/ml/permissions/read_ml_access.ts | 1 + .../functional_with_es_ssl/apps/ml/index.ts | 4 +++- .../functional/tests/feature_control.ts | 1 + .../async_search/sessions_in_space.ts | 8 +++++-- .../tests/apps/discover/sessions_in_space.ts | 8 +++++-- .../sessions_management_permissions.ts | 8 +++++-- .../login_selector/auth_provider_hint.ts | 1 + .../login_selector/basic_functionality.ts | 1 + .../apps/endpoint/endpoint_permissions.ts | 1 + .../visual_regression/tests/login_page.ts | 1 + 54 files changed, 144 insertions(+), 70 deletions(-) rename x-pack/test/functional/apps/security/{doc_level_security_roles.js => doc_level_security_roles.ts} (91%) rename x-pack/test/functional/apps/security/{field_level_security.js => field_level_security.ts} (94%) rename x-pack/test/functional/apps/security/{index.js => index.ts} (85%) rename x-pack/test/functional/apps/security/{management.js => management.ts} (95%) rename x-pack/test/functional/apps/security/{secure_roles_perm.js => secure_roles_perm.ts} (93%) rename x-pack/test/functional/apps/security/{user_email.js => user_email.ts} (91%) diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/login_page.ts index 8de4a47e10b1e..407c4dbee464c 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/login_page.ts @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); afterEach(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index 4babe0bd6ff88..fd05d2af07747 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -33,6 +33,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); diff --git a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts index c9088c650c033..b5ee6a1948599 100644 --- a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts +++ b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @@ -72,10 +72,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); - await ml.securityUI.logout(); }); for (const testData of testDataList) { diff --git a/x-pack/test/accessibility/apps/transform.ts b/x-pack/test/accessibility/apps/transform.ts index 4c58887f003b2..59f19471490b8 100644 --- a/x-pack/test/accessibility/apps/transform.ts +++ b/x-pack/test/accessibility/apps/transform.ts @@ -30,6 +30,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await transform.securityUI.logout(); }); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 20c79d9142f09..fb6f34aa24a14 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -52,6 +52,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_advanced_settings_all_role'), diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index 05e13bb04d91b..b5a206a43aeb6 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -23,6 +23,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index 983a3101b9e31..1497c85b91bad 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -58,6 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_canvas_all_role'), diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index e7aa3e6a54e60..1b1aa9abc831a 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -43,12 +43,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await esArchiver.unload( 'x-pack/test/functional/es_archives/dashboard/feature_controls/security' ); - - // logout, so the other tests don't accidentally run as the custom users we're testing below - await PageObjects.security.forceLogout(); }); describe('global dashboard all privileges, no embeddable application privileges', () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts index 1a5f8c34a183c..f6692a2edb3bf 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts @@ -70,15 +70,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await security.role.delete('dashboard_write_vis_read'); await security.user.delete('dashboard_write_vis_read_user'); await esArchiver.unload( 'x-pack/test/functional/es_archives/dashboard/feature_controls/security' ); - - // logout, so the other tests don't accidentally run as the custom users we're testing below - await PageObjects.security.forceLogout(); }); describe('lens by value works without library save permissions', () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 1575948610566..f7ded778660fa 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -27,6 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 8ebf277d63cbe..0a12de3fb44d6 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -43,12 +43,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/security' ); - - // logout, so the other tests don't accidentally run as the custom users we're testing below - await PageObjects.security.forceLogout(); }); describe('global discover all privileges', () => { diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index 69f2f585d8dba..9179373cf610c 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -26,6 +26,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/functional/apps/home/feature_controls/home_security.ts b/x-pack/test/functional/apps/home/feature_controls/home_security.ts index 92d477a92f270..f306b9f553d64 100644 --- a/x-pack/test/functional/apps/home/feature_controls/home_security.ts +++ b/x-pack/test/functional/apps/home/feature_controls/home_security.ts @@ -26,12 +26,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await esArchiver.unload( 'x-pack/test/functional/es_archives/dashboard/feature_controls/security' ); - - // logout, so the other tests don't accidentally run as the custom users we're testing below - await PageObjects.security.forceLogout(); }); describe('global all privileges', () => { diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index d04ec8f4d66b4..20dd08fab1496 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -64,6 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_index_patterns_all_role'), diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index d6e8737b72b91..f713c903ebe1e 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -53,6 +53,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_infrastructure_all_role'), @@ -150,6 +151,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_infrastructure_read_role'), diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index d4f56ee3c9b9b..8908a34298373 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -50,6 +50,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_logs_all_role'), @@ -112,6 +113,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_logs_read_role'), @@ -174,6 +176,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_logs_no_privileges_role'), diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index dcd82ea05ccf3..db6bfb642ebbb 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -20,6 +20,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('maps security feature controls', () => { after(async () => { // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); @@ -53,6 +54,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_maps_all_role'), diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts index 0398bacd1e664..2f00a5f6d0f91 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts @@ -21,6 +21,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); PageObjects.maps.setBasePath(''); }); diff --git a/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts b/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts index 63912b7af5557..58af3abbf6a47 100644 --- a/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts @@ -32,10 +32,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await security.role.delete('global_all_role'); - // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); + + await security.role.delete('global_all_role'); }); describe('machine_learning_user', () => { diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index ee14e3f414e36..2e3a29d50dd11 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -21,6 +21,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); await ml.testResources.deleteSavedSearches(); @@ -42,7 +45,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); await ml.testResources.resetKibanaTimeZone(); - await ml.securityUI.logout(); }); loadTestFile(require.resolve('./permissions')); @@ -62,6 +64,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); await ml.testResources.deleteSavedSearches(); @@ -83,7 +88,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); await ml.testResources.resetKibanaTimeZone(); - await ml.securityUI.logout(); }); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 356e382217964..d478229902aff 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -32,6 +32,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); @@ -156,6 +157,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 33ec80f16225e..6132e6e63b1b0 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -25,6 +25,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index be57904b94451..b5aa7a84af8a1 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -32,6 +32,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); @@ -157,6 +158,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts index bf83892ce1934..80483503982b1 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -36,11 +36,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); - await security.role.delete('global_all_role'); - // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); + + await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); + await security.role.delete('global_all_role'); }); describe('monitoring_user', () => { diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts index 71f100b49068f..4450d8c280a88 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -21,9 +21,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); - await PageObjects.common.navigateToApp('home'); + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); + await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); }); describe('space with no features disabled', () => { diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index 0eaf4893e072d..8b6474b0679b7 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -58,6 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_all_role'), @@ -176,6 +177,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_som_read_role'), @@ -311,6 +313,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_visualize_all_role'), diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.ts similarity index 91% rename from x-pack/test/functional/apps/security/doc_level_security_roles.js rename to x-pack/test/functional/apps/security/doc_level_security_roles.ts index 2cbaae144d722..88a16b002d7fb 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.ts @@ -7,8 +7,9 @@ import expect from '@kbn/expect'; import { keyBy } from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const retry = getService('retry'); @@ -40,15 +41,12 @@ export default function ({ getService, getPageObjects }) { }, ], }, - kibana: { - global: ['all'], - }, }); const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); log.debug('actualRoles = %j', roles); expect(roles).to.have.key('myroleEast'); expect(roles.myroleEast.reserved).to.be(false); - screenshot.take('Security_Roles'); + await screenshot.take('Security_Roles'); }); it('should add new user userEAST ', async function () { @@ -78,6 +76,7 @@ export default function ({ getService, getPageObjects }) { expect(rowData).to.contain('EAST'); }); after('logout', async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); }); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.ts similarity index 94% rename from x-pack/test/functional/apps/security/field_level_security.js rename to x-pack/test/functional/apps/security/field_level_security.ts index 1f8ecb0df202c..917d41bdbb377 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.ts @@ -7,8 +7,9 @@ import expect from '@kbn/expect'; import { keyBy } from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const retry = getService('retry'); @@ -18,7 +19,7 @@ export default function ({ getService, getPageObjects }) { describe('field_level_security', () => { before('initialize tests', async () => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/security/flstest/data'); //( data) + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/security/flstest/data'); // ( data) await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/security/flstest/index_pattern' ); @@ -40,9 +41,6 @@ export default function ({ getService, getPageObjects }) { }, ], }, - kibana: { - global: ['all'], - }, }); await PageObjects.common.sleep(1000); @@ -63,9 +61,6 @@ export default function ({ getService, getPageObjects }) { }, ], }, - kibana: { - global: ['all'], - }, }); await PageObjects.common.sleep(1000); const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename'); @@ -127,6 +122,7 @@ export default function ({ getService, getPageObjects }) { }); after(async function () { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/security/flstest/index_pattern' diff --git a/x-pack/test/functional/apps/security/index.js b/x-pack/test/functional/apps/security/index.ts similarity index 85% rename from x-pack/test/functional/apps/security/index.js rename to x-pack/test/functional/apps/security/index.ts index 188f49e300256..3b4c6989d38fa 100644 --- a/x-pack/test/functional/apps/security/index.js +++ b/x-pack/test/functional/apps/security/index.ts @@ -5,7 +5,9 @@ * 2.0. */ -export default function ({ loadTestFile }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('security app', function () { this.tags('ciGroup4'); diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.ts similarity index 95% rename from x-pack/test/functional/apps/security/management.js rename to x-pack/test/functional/apps/security/management.ts index 3e6ee3a2f8867..c6f25ad30bafb 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); @@ -45,9 +46,11 @@ export default function ({ getService, getPageObjects }) { }); after(async () => { - await security.role.delete('logstash-readonly'); - await security.user.delete('dashuser', 'new-user'); + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); + await security.role.delete('logstash-readonly'); + await security.user.delete('dashuser'); + await security.user.delete('new-user'); }); describe('Security', () => { diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.ts similarity index 93% rename from x-pack/test/functional/apps/security/secure_roles_perm.js rename to x-pack/test/functional/apps/security/secure_roles_perm.ts index 0c1dbfd5f826a..c9e7bb6e4da6c 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.ts @@ -7,8 +7,9 @@ import expect from '@kbn/expect'; import { keyBy } from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ 'security', 'settings', @@ -48,9 +49,6 @@ export default function ({ getService, getPageObjects }) { }, ], }, - kibana: { - global: ['all'], - }, }); }); @@ -89,6 +87,7 @@ export default function ({ getService, getPageObjects }) { }); after(async function () { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' diff --git a/x-pack/test/functional/apps/security/security.ts b/x-pack/test/functional/apps/security/security.ts index 8cee43d0f3c11..11e1525eb4bfd 100644 --- a/x-pack/test/functional/apps/security/security.ts +++ b/x-pack/test/functional/apps/security/security.ts @@ -29,6 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); afterEach(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.ts similarity index 91% rename from x-pack/test/functional/apps/security/user_email.js rename to x-pack/test/functional/apps/security/user_email.ts index 84566c1a6f5ff..65bf111ceedbf 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.ts @@ -7,8 +7,9 @@ import expect from '@kbn/expect'; import { keyBy } from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'settings', 'common', 'accountSetting']); const log = getService('log'); const kibanaServer = getService('kibanaServer'); @@ -57,6 +58,7 @@ export default function ({ getService, getPageObjects }) { }); after(async function () { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/security/discover' diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index f708282393d83..b371fb2b84aea 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -26,6 +26,7 @@ export default function enterSpaceFunctonalTests({ ); afterEach(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 78207c49c9b75..101a24754898e 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -57,10 +57,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_all_role'), security.user.delete('global_all_user'), - PageObjects.security.forceLogout(), ]); }); @@ -134,10 +135,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('default_space_all_role'), security.user.delete('default_space_all_user'), - PageObjects.security.forceLogout(), ]); }); @@ -213,10 +215,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await Promise.all([ security.role.delete('nondefault_space_specific_role'), security.user.delete('nondefault_space_specific_user'), - PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index bab90a6d567fe..ef554bb80ebc4 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -34,6 +34,7 @@ export default function spaceSelectorFunctionalTests({ ); afterEach(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index f0505755065f7..90bb95fd6b3e8 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -24,6 +24,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await transform.securityUI.logout(); + await transform.securityCommon.cleanTransformUsers(); await transform.securityCommon.cleanTransformRoles(); @@ -36,7 +39,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); await transform.testResources.resetKibanaTimeZone(); - await transform.securityUI.logout(); }); loadTestFile(require.resolve('./permissions')); diff --git a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts index 5f74b2da213b0..423b179e35627 100644 --- a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts @@ -20,6 +20,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await transform.securityUI.logout(); }); diff --git a/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts index 6a04d33ff152d..ed9b9cb1dec90 100644 --- a/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/read_transform_access.ts @@ -20,6 +20,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await transform.securityUI.logout(); }); diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index 977a384062f79..c1ba546864a53 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -25,6 +25,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index d089ab47c0cf7..08137f167badf 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -37,9 +37,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/visualize/default'); // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); + + await esArchiver.unload('x-pack/test/functional/es_archives/visualize/default'); }); describe('global visualize all privileges', () => { @@ -76,6 +78,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await security.role.delete('global_visualize_all_role'); await security.user.delete('global_visualize_all_user'); @@ -207,6 +210,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await security.role.delete('global_visualize_read_role'); await security.user.delete('global_visualize_read_user'); @@ -322,6 +326,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await security.role.delete('global_visualize_read_url_create_role'); await security.user.delete('global_visualize_read_url_create_user'); @@ -427,6 +432,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); await security.role.delete('no_visualize_privileges_role'); await security.user.delete('no_visualize_privileges_user'); diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 274de53c5f3fd..fbdb918b217f2 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -540,7 +540,10 @@ export class SecurityPageObject extends FtrService { return confirmText; } - async addRole(roleName: string, roleObj: Role) { + async addRole( + roleName: string, + roleObj: { elasticsearch: Pick } + ) { const self = this; await this.clickCreateNewRole(); @@ -558,21 +561,14 @@ export class SecurityPageObject extends FtrService { await this.monacoEditor.setCodeEditorValue(roleObj.elasticsearch.indices[0].query); } - const globalPrivileges = (roleObj.kibana as any).global; - if (globalPrivileges) { - for (const privilegeName of globalPrivileges) { - await this.testSubjects.click('addSpacePrivilegeButton'); + await this.testSubjects.click('addSpacePrivilegeButton'); + await this.testSubjects.click('spaceSelectorComboBox'); - await this.testSubjects.click('spaceSelectorComboBox'); + const globalSpaceOption = await this.find.byCssSelector(`#spaceOption_\\*`); + await globalSpaceOption.click(); - const globalSpaceOption = await this.find.byCssSelector(`#spaceOption_\\*`); - await globalSpaceOption.click(); - - await this.testSubjects.click(`basePrivilege_${privilegeName}`); - - await this.testSubjects.click('createSpacePrivilegeButton'); - } - } + await this.testSubjects.click('basePrivilege_all'); + await this.testSubjects.click('createSpacePrivilegeButton'); const addPrivilege = (privileges: string[]) => { return privileges.reduce((promise: Promise, privilegeName: string) => { diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index 901744719144c..b44c5f08bdbc6 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -53,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts index 12fc7b8122c99..feb251cc26e1d 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts @@ -23,6 +23,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index 0f271719a0d0f..c1b13d6dc1f11 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -53,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await ml.securityUI.logout(); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/index.ts b/x-pack/test/functional_with_es_ssl/apps/ml/index.ts index e3a390a5c5486..fac9e46dcb65b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/ml/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/ml/index.ts @@ -20,12 +20,14 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + await ml.testResources.deleteIndexPatternByTitle('ft_ecommerce'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); await ml.testResources.resetKibanaTimeZone(); - await ml.securityUI.logout(); }); loadTestFile(require.resolve('./alert_flyout')); diff --git a/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts b/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts index 1f6197b02afa3..0dbc3fae3e9c6 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts @@ -53,6 +53,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index 98eca99ff436c..8561094890474 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -57,11 +57,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await security.role.delete('data_analyst'); await security.user.delete('analyst'); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); - await PageObjects.security.forceLogout(); }); it('Saves and restores a session', async () => { @@ -127,11 +129,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await security.role.delete('data_analyst'); await security.user.delete('analyst'); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); - await PageObjects.security.forceLogout(); }); it("Doesn't allow to store a session", async () => { diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts index 064c6bdc4495e..b989ad1127306 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts @@ -57,11 +57,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await security.role.delete('data_analyst'); await security.user.delete('analyst'); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); - await PageObjects.security.forceLogout(); }); it('Saves and restores a session', async () => { @@ -130,11 +132,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await security.role.delete('data_analyst'); await security.user.delete('analyst'); await esArchiver.unload('x-pack/test/functional/es_archives/dashboard/session_in_space'); - await PageObjects.security.forceLogout(); }); it("Doesn't allow to store a session", async () => { diff --git a/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts b/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts index ad22fd2cbaf71..0eeec2a683d66 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts @@ -51,9 +51,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await security.role.delete('data_analyst'); await security.user.delete('analyst'); - await PageObjects.security.forceLogout(); }); it('if no apps enable search sessions', async () => { @@ -90,9 +92,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await PageObjects.security.forceLogout(); + await security.role.delete('data_analyst'); await security.user.delete('analyst'); - await PageObjects.security.forceLogout(); }); it('if one app enables search sessions', async () => { diff --git a/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts b/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts index b19af792d8af6..1d4aa6b5819cb 100644 --- a/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts +++ b/x-pack/test/security_functional/tests/login_selector/auth_provider_hint.ts @@ -47,6 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); afterEach(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts index b8c0859541eb9..4483ed6f5a5cc 100644 --- a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts @@ -48,6 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); afterEach(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts index 48c0aea825048..672c9a7c78d27 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts @@ -50,6 +50,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { after(async () => { // Log the user back out + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); // delete role/user diff --git a/x-pack/test/visual_regression/tests/login_page.ts b/x-pack/test/visual_regression/tests/login_page.ts index 65effd45d65df..34e1132134744 100644 --- a/x-pack/test/visual_regression/tests/login_page.ts +++ b/x-pack/test/visual_regression/tests/login_page.ts @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); afterEach(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior await PageObjects.security.forceLogout(); }); From 433ffa5876dd928030521c0dcf97b4581fb7945f Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 9 Nov 2021 12:07:02 +0100 Subject: [PATCH 25/98] [Discover][Context] Fix selecting another surrounding document (#117567) --- .../application/context/context_app.tsx | 16 ++++++--- .../context/utils/use_context_app_fetch.tsx | 12 ++++--- .../apps/context/_discover_navigation.ts | 35 ++++++++++++++----- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index 293680b85c005..1bda31bd7bd27 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -50,21 +50,29 @@ export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { /** * Context fetched state */ - const { fetchedState, fetchContextRows, fetchAllRows, fetchSurroundingRows } = useContextAppFetch( - { + const { fetchedState, fetchContextRows, fetchAllRows, fetchSurroundingRows, resetFetchedState } = + useContextAppFetch({ anchorId, indexPattern, appState, useNewFieldsApi, services, + }); + /** + * Reset state when anchor changes + */ + useEffect(() => { + if (prevAppState.current) { + prevAppState.current = undefined; + resetFetchedState(); } - ); + }, [anchorId, resetFetchedState]); /** * Fetch docs on ui changes */ useEffect(() => { - if (!prevAppState.current || fetchedState.anchor._id !== anchorId) { + if (!prevAppState.current) { fetchAllRows(); } else if (prevAppState.current.predecessorCount !== appState.predecessorCount) { fetchSurroundingRows(SurrDocType.PREDECESSORS); diff --git a/src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx b/src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx index acc11ccdbe8f9..e5ed24d475497 100644 --- a/src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx @@ -160,15 +160,19 @@ export function useContextAppFetch({ [fetchSurroundingRows] ); - const fetchAllRows = useCallback( - () => fetchAnchorRow().then((anchor) => anchor && fetchContextRows(anchor)), - [fetchAnchorRow, fetchContextRows] - ); + const fetchAllRows = useCallback(() => { + fetchAnchorRow().then((anchor) => anchor && fetchContextRows(anchor)); + }, [fetchAnchorRow, fetchContextRows]); + + const resetFetchedState = useCallback(() => { + setFetchedState(getInitialContextQueryState()); + }, []); return { fetchedState, fetchAllRows, fetchContextRows, fetchSurroundingRows, + resetFetchedState, }; } diff --git a/test/functional/apps/context/_discover_navigation.ts b/test/functional/apps/context/_discover_navigation.ts index 60745bd64b8be..9a807293c8148 100644 --- a/test/functional/apps/context/_discover_navigation.ts +++ b/test/functional/apps/context/_discover_navigation.ts @@ -55,20 +55,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({}); }); - it('should open the context view with the selected document as anchor', async () => { + it('should open the context view with the selected document as anchor and allows selecting next anchor', async () => { + /** + * Helper function to get the first timestamp of the document table + * @param isAnchorRow - determins if just the anchor row of context should be selected + */ + const getTimestamp = async (isAnchorRow: boolean = false) => { + const contextFields = await docTable.getFields({ isAnchorRow }); + return contextFields[0][0]; + }; + // get the timestamp of the first row + + const firstDiscoverTimestamp = await getTimestamp(); + // check the anchor timestamp in the context view await retry.waitFor('selected document timestamp matches anchor timestamp ', async () => { - // get the timestamp of the first row - const discoverFields = await docTable.getFields(); - const firstTimestamp = discoverFields[0][0]; - // navigate to the context view await docTable.clickRowToggle({ rowIndex: 0 }); const rowActions = await docTable.getRowActions({ rowIndex: 0 }); await rowActions[0].click(); - const contextFields = await docTable.getFields({ isAnchorRow: true }); - const anchorTimestamp = contextFields[0][0]; - return anchorTimestamp === firstTimestamp; + await PageObjects.context.waitUntilContextLoadingHasFinished(); + const anchorTimestamp = await getTimestamp(true); + return anchorTimestamp === firstDiscoverTimestamp; + }); + + await retry.waitFor('next anchor timestamp matches previous anchor timestamp', async () => { + // get the timestamp of the first row + const firstContextTimestamp = await getTimestamp(false); + await docTable.clickRowToggle({ rowIndex: 0 }); + const rowActions = await docTable.getRowActions({ rowIndex: 0 }); + await rowActions[0].click(); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + const anchorTimestamp = await getTimestamp(true); + return anchorTimestamp === firstContextTimestamp; }); }); From e6dc051b86a98a00ae32d35876db7b92f5b37737 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Tue, 9 Nov 2021 12:11:32 +0100 Subject: [PATCH 26/98] [Expressions] Fix expressions execution abortion to prevent performance issues (#117714) --- .../expressions/common/execution/execution.ts | 103 +++++++++--------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 54a4800ec7c34..90e05083fd9f1 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -16,7 +16,6 @@ import { from, isObservable, of, - race, throwError, Observable, ReplaySubject, @@ -25,7 +24,7 @@ import { catchError, finalize, map, pluck, shareReplay, switchMap, tap } from 'r import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; -import { abortSignalToPromise, now } from '../../../kibana_utils/common'; +import { now, AbortError } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { @@ -50,13 +49,6 @@ type UnwrapReturnType unknown> = ? UnwrapObservable> : UnwrapPromiseOrReturn>; -// type ArgumentsOf = Function extends ExpressionFunction< -// unknown, -// infer Arguments -// > -// ? Arguments -// : never; - /** * The result returned after an expression function execution. */ @@ -95,6 +87,51 @@ const createAbortErrorValue = () => name: 'AbortError', }); +function markPartial() { + return (source: Observable) => + new Observable>((subscriber) => { + let latest: ExecutionResult | undefined; + + subscriber.add( + source.subscribe({ + next: (result) => { + latest = { result, partial: true }; + subscriber.next(latest); + }, + error: (error) => subscriber.error(error), + complete: () => { + if (latest) { + latest.partial = false; + } + + subscriber.complete(); + }, + }) + ); + + subscriber.add(() => { + latest = undefined; + }); + }); +} + +function takeUntilAborted(signal: AbortSignal) { + return (source: Observable) => + new Observable((subscriber) => { + const throwAbortError = () => { + subscriber.error(new AbortError()); + }; + + subscriber.add(source.subscribe(subscriber)); + subscriber.add(() => signal.removeEventListener('abort', throwAbortError)); + + signal.addEventListener('abort', throwAbortError); + if (signal.aborted) { + throwAbortError(); + } + }); +} + export interface ExecutionParams { executor: Executor; ast?: ExpressionAstExpression; @@ -138,18 +175,6 @@ export class Execution< */ private readonly abortController = getNewAbortController(); - /** - * Promise that rejects if/when abort controller sends "abort" signal. - */ - private readonly abortRejection = abortSignalToPromise(this.abortController.signal); - - /** - * Races a given observable against the "abort" event of `abortController`. - */ - private race(observable: Observable): Observable { - return race(from(this.abortRejection.promise), observable); - } - /** * Whether .start() method has been called. */ @@ -221,32 +246,9 @@ export class Execution< this.result = this.input$.pipe( switchMap((input) => - this.race(this.invokeChain(this.state.get().ast.chain, input)).pipe( - (source) => - new Observable>((subscriber) => { - let latest: ExecutionResult | undefined; - - subscriber.add( - source.subscribe({ - next: (result) => { - latest = { result, partial: true }; - subscriber.next(latest); - }, - error: (error) => subscriber.error(error), - complete: () => { - if (latest) { - latest.partial = false; - } - - subscriber.complete(); - }, - }) - ); - - subscriber.add(() => { - latest = undefined; - }); - }) + this.invokeChain(this.state.get().ast.chain, input).pipe( + takeUntilAborted(this.abortController.signal), + markPartial() ) ), catchError((error) => { @@ -265,7 +267,6 @@ export class Execution< }, error: (error) => this.state.transitions.setError(error), }), - finalize(() => this.abortRejection.cleanup()), shareReplay(1) ); } @@ -356,9 +357,9 @@ export class Execution< // `resolveArgs` returns an object because the arguments themselves might // actually have `then` or `subscribe` methods which would be treated as a `Promise` // or an `Observable` accordingly. - return this.race(this.resolveArgs(fn, currentInput, fnArgs)).pipe( + return this.resolveArgs(fn, currentInput, fnArgs).pipe( tap((args) => this.execution.params.debug && Object.assign(link.debug, { args })), - switchMap((args) => this.race(this.invokeFunction(fn, currentInput, args))), + switchMap((args) => this.invokeFunction(fn, currentInput, args)), switchMap((output) => (getType(output) === 'error' ? throwError(output) : of(output))), tap((output) => this.execution.params.debug && Object.assign(link.debug, { output })), catchError((rawError) => { @@ -390,7 +391,7 @@ export class Execution< ): Observable> { return of(input).pipe( map((currentInput) => this.cast(currentInput, fn.inputTypes)), - switchMap((normalizedInput) => this.race(of(fn.fn(normalizedInput, args, this.context)))), + switchMap((normalizedInput) => of(fn.fn(normalizedInput, args, this.context))), switchMap( (fnResult) => (isObservable(fnResult) From 1837e4a8544430b49a694fb720fd93ff01595120 Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:00:54 +0000 Subject: [PATCH 27/98] Fix flaky enter spaces tests (#117510) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/functional/apps/spaces/enter_space.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index b371fb2b84aea..e1dc70b81e146 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -14,8 +14,7 @@ export default function enterSpaceFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'spaceSelector']); - // FLAKY: https://github.com/elastic/kibana/issues/100570 - describe.skip('Enter Space', function () { + describe('Enter Space', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/spaces/enter_space'); From 06ab7848ffd9fdd745decb50fe897f9b3e39f279 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Tue, 9 Nov 2021 07:39:14 -0500 Subject: [PATCH 28/98] [Security Solution][Investigations] - Swap count and histogram order (#117878) --- .../pages/detection_engine/detection_engine.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 204387f4a241b..bcff80778475e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -359,6 +359,13 @@ const DetectionEnginePageComponent: React.FC = ({ + + + = ({ updateDateRange={updateDateRangeCallback} /> - - - - From b7173dd1a3e0c561eab81f08e1d0b184717773ac Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 9 Nov 2021 07:53:46 -0500 Subject: [PATCH 29/98] adjust yaxis mode according to field format (#117646) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../configurations/lens_attributes.test.ts | 5 +++-- .../configurations/lens_attributes.ts | 14 +++++++++++++- .../configurations/test_data/sample_attribute.ts | 1 + .../test_data/sample_attribute_kpi.ts | 1 + 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 139f9fe67c751..135cf3c59a1ce 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -9,6 +9,7 @@ import { LayerConfig, LensAttributes } from './lens_attributes'; import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; import { sampleAttribute } from './test_data/sample_attribute'; + import { LCP_FIELD, TRANSACTION_DURATION, @@ -467,7 +468,7 @@ describe('Lens Attribute', () => { palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0', axisMode: 'left' }], }, ], legend: { isVisible: true, showSingleSeries: true, position: 'right' }, @@ -510,7 +511,7 @@ describe('Lens Attribute', () => { seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0', axisMode: 'left' }], }, ]); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 5e769882a2793..3e6e6d9cb83b0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -732,7 +732,19 @@ export class LensAttributes { seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, palette: layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ - { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color }, + { + forAccessor: `y-axis-column-layer${index}`, + color: layerConfig.color, + /* if the fields format matches the field format of the first layer, use the default y axis (right) + * if not, use the secondary y axis (left) */ + axisMode: + layerConfig.indexPattern.fieldFormatMap[layerConfig.selectedMetricField]?.id === + this.layerConfigs[0].indexPattern.fieldFormatMap[ + this.layerConfigs[0].selectedMetricField + ]?.id + ? 'left' + : 'right', + }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 8254a5a816921..cfbd2a5df0358 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -181,6 +181,7 @@ export const sampleAttribute = { { color: 'green', forAccessor: 'y-axis-column-layer0', + axisMode: 'left', }, ], }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 8fbda9f6adc52..668049dcc122b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -83,6 +83,7 @@ export const sampleAttributeKpi = { { color: 'green', forAccessor: 'y-axis-column-layer0', + axisMode: 'left', }, ], }, From 7f50f3435879d0abef843e9d5a57128a5a943206 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Tue, 9 Nov 2021 08:30:36 -0500 Subject: [PATCH 30/98] [App Search] Use paginated API for Crawler domains (#117587) --- .../crawler/components/domains_table.test.tsx | 30 ++- .../crawler/components/domains_table.tsx | 28 ++- .../crawler/crawler_domains_logic.test.ts | 206 ++++++++++++++++++ .../crawler/crawler_domains_logic.ts | 125 +++++++++++ .../components/crawler/crawler_logic.test.ts | 11 + .../components/crawler/crawler_logic.ts | 5 + .../crawler/crawler_overview_logic.test.ts | 93 -------- .../crawler/crawler_overview_logic.ts | 50 ----- .../crawler/crawler_single_domain.test.tsx | 3 - .../server/routes/app_search/crawler.test.ts | 39 ++++ .../server/routes/app_search/crawler.ts | 18 ++ 11 files changed, 448 insertions(+), 160 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx index 76622f9c12822..7511f4ae2c2c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx @@ -12,8 +12,9 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiBasicTable, EuiButtonIcon, EuiInMemoryTable } from '@elastic/eui'; +import { EuiBasicTable, EuiButtonIcon } from '@elastic/eui'; +import { DEFAULT_META } from '../../../../shared/constants'; import { mountWithIntl } from '../../../../test_helpers'; import { CrawlerDomain } from '../types'; @@ -51,15 +52,19 @@ const domains: CrawlerDomain[] = [ const values = { // EngineLogic engineName: 'some-engine', - // CrawlerOverviewLogic + // CrawlerDomainsLogic domains, + meta: DEFAULT_META, + dataLoading: false, // AppLogic myRole: { canManageEngineCrawler: false }, }; const actions = { - // CrawlerOverviewLogic + // CrawlerDomainsLogic deleteDomain: jest.fn(), + fetchCrawlerDomainsData: jest.fn(), + onPaginate: jest.fn(), }; describe('DomainsTable', () => { @@ -69,17 +74,28 @@ describe('DomainsTable', () => { beforeEach(() => { jest.clearAllMocks(); }); + beforeAll(() => { setMockValues(values); setMockActions(actions); wrapper = shallow(); tableContent = mountWithIntl() - .find(EuiInMemoryTable) + .find(EuiBasicTable) .text(); }); it('renders', () => { - expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + expect(wrapper.find(EuiBasicTable)).toHaveLength(1); + + expect(wrapper.find(EuiBasicTable).prop('pagination')).toEqual({ + hidePerPageOptions: true, + pageIndex: 0, + pageSize: 10, + totalItemCount: 0, + }); + + wrapper.find(EuiBasicTable).simulate('change', { page: { index: 2 } }); + expect(actions.onPaginate).toHaveBeenCalledWith(3); }); describe('columns', () => { @@ -88,7 +104,7 @@ describe('DomainsTable', () => { }); it('renders a clickable domain url', () => { - const basicTable = wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive(); + const basicTable = wrapper.find(EuiBasicTable).dive(); const link = basicTable.find('[data-test-subj="CrawlerDomainURL"]').at(0); expect(link.dive().text()).toContain('elastic.co'); @@ -110,7 +126,7 @@ describe('DomainsTable', () => { }); describe('actions column', () => { - const getTable = () => wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive(); + const getTable = () => wrapper.find(EuiBasicTable).dive(); const getActions = () => getTable().find('ExpandedItemActions'); const getActionItems = () => getActions().first().dive().find('DefaultItemAction'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx index 1f0f6be22102f..b8d8159be7b16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiBasicTableColumn, EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -18,11 +18,11 @@ import { FormattedNumber } from '@kbn/i18n/react'; import { DELETE_BUTTON_LABEL, MANAGE_BUTTON_LABEL } from '../../../../shared/constants'; import { KibanaLogic } from '../../../../shared/kibana'; import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; import { AppLogic } from '../../../app_logic'; import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../routes'; import { generateEnginePath } from '../../engine'; -import { CrawlerLogic } from '../crawler_logic'; -import { CrawlerOverviewLogic } from '../crawler_overview_logic'; +import { CrawlerDomainsLogic } from '../crawler_domains_logic'; import { CrawlerDomain } from '../types'; import { getDeleteDomainConfirmationMessage } from '../utils'; @@ -30,9 +30,12 @@ import { getDeleteDomainConfirmationMessage } from '../utils'; import { CustomFormattedTimestamp } from './custom_formatted_timestamp'; export const DomainsTable: React.FC = () => { - const { domains } = useValues(CrawlerLogic); + const { domains, meta, dataLoading } = useValues(CrawlerDomainsLogic); + const { fetchCrawlerDomainsData, onPaginate, deleteDomain } = useActions(CrawlerDomainsLogic); - const { deleteDomain } = useActions(CrawlerOverviewLogic); + useEffect(() => { + fetchCrawlerDomainsData(); + }, [meta.page.current]); const { myRole: { canManageEngineCrawler }, @@ -125,5 +128,16 @@ export const DomainsTable: React.FC = () => { columns.push(actionsColumn); } - return ; + return ( + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts new file mode 100644 index 0000000000000..6cf2f21fc6d2e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts @@ -0,0 +1,206 @@ +/* + * 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 { + LogicMounter, + mockHttpValues, + mockFlashMessageHelpers, +} from '../../../__mocks__/kea_logic'; +import '../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { Meta } from '../../../../../common/types'; + +import { DEFAULT_META } from '../../../shared/constants'; + +import { CrawlerDomainsLogic, CrawlerDomainsValues } from './crawler_domains_logic'; +import { CrawlerDataFromServer, CrawlerDomain, CrawlerDomainFromServer } from './types'; +import { crawlerDataServerToClient } from './utils'; + +const DEFAULT_VALUES: CrawlerDomainsValues = { + dataLoading: true, + domains: [], + meta: DEFAULT_META, +}; + +const crawlerDataResponse: CrawlerDataFromServer = { + domains: [ + { + id: '507f1f77bcf86cd799439011', + name: 'elastic.co', + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + document_count: 13, + sitemaps: [], + entry_points: [], + crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], + }, + ], + events: [], + most_recent_crawl_request: null, +}; + +const clientCrawlerData = crawlerDataServerToClient(crawlerDataResponse); + +const domainsFromServer: CrawlerDomainFromServer[] = [ + { + name: 'http://www.example.com', + created_on: 'foo', + document_count: 10, + id: '1', + crawl_rules: [], + entry_points: [], + sitemaps: [], + deduplication_enabled: true, + deduplication_fields: [], + available_deduplication_fields: [], + }, +]; + +const domains: CrawlerDomain[] = [ + { + createdOn: 'foo', + documentCount: 10, + id: '1', + url: 'http://www.example.com', + crawlRules: [], + entryPoints: [], + sitemaps: [], + deduplicationEnabled: true, + deduplicationFields: [], + availableDeduplicationFields: [], + }, +]; + +const meta: Meta = { + page: { + current: 2, + size: 100, + total_pages: 5, + total_results: 500, + }, +}; + +describe('CrawlerDomainsLogic', () => { + const { mount } = new LogicMounter(CrawlerDomainsLogic); + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(CrawlerDomainsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onReceiveData', () => { + it('sets state from an API call', () => { + mount(); + + CrawlerDomainsLogic.actions.onReceiveData(domains, meta); + + expect(CrawlerDomainsLogic.values).toEqual({ + ...DEFAULT_VALUES, + domains, + meta, + dataLoading: false, + }); + }); + }); + + describe('onPaginate', () => { + it('sets dataLoading to true & sets meta state', () => { + mount({ dataLoading: false }); + CrawlerDomainsLogic.actions.onPaginate(5); + + expect(CrawlerDomainsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + meta: { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + current: 5, + }, + }, + }); + }); + }); + }); + + describe('listeners', () => { + describe('fetchCrawlerDomainsData', () => { + it('updates logic with data that has been converted from server to client', async () => { + mount(); + jest.spyOn(CrawlerDomainsLogic.actions, 'onReceiveData'); + + http.get.mockReturnValueOnce( + Promise.resolve({ + results: domainsFromServer, + meta, + }) + ); + + CrawlerDomainsLogic.actions.fetchCrawlerDomainsData(); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/crawler/domains', + { + query: { 'page[current]': 1, 'page[size]': 10 }, + } + ); + expect(CrawlerDomainsLogic.actions.onReceiveData).toHaveBeenCalledWith(domains, meta); + }); + + it('displays any errors to the user', async () => { + mount(); + http.get.mockReturnValueOnce(Promise.reject('error')); + + CrawlerDomainsLogic.actions.fetchCrawlerDomainsData(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('deleteDomain', () => { + it('deletes the domain and then calls crawlerDomainDeleted with the response', async () => { + jest.spyOn(CrawlerDomainsLogic.actions, 'crawlerDomainDeleted'); + http.delete.mockReturnValue(Promise.resolve(crawlerDataResponse)); + + CrawlerDomainsLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); + await nextTick(); + + expect(http.delete).toHaveBeenCalledWith( + '/internal/app_search/engines/some-engine/crawler/domains/1234', + { + query: { respond_with: 'crawler_details' }, + } + ); + expect(CrawlerDomainsLogic.actions.crawlerDomainDeleted).toHaveBeenCalledWith( + clientCrawlerData + ); + }); + + it('calls flashApiErrors when there is an error', async () => { + http.delete.mockReturnValue(Promise.reject('error')); + + CrawlerDomainsLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.ts new file mode 100644 index 0000000000000..e26e9528ee1d2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { Meta } from '../../../../../common/types'; +import { DEFAULT_META } from '../../../shared/constants'; +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { updateMetaPageIndex } from '../../../shared/table_pagination'; +import { EngineLogic } from '../engine'; + +import { + CrawlerData, + CrawlerDataFromServer, + CrawlerDomain, + CrawlerDomainFromServer, +} from './types'; +import { crawlerDataServerToClient, crawlerDomainServerToClient } from './utils'; + +export interface CrawlerDomainsValues { + dataLoading: boolean; + domains: CrawlerDomain[]; + meta: Meta; +} + +interface CrawlerDomainsResponse { + results: CrawlerDomainFromServer[]; + meta: Meta; +} + +interface CrawlerDomainsActions { + deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain }; + fetchCrawlerDomainsData(): void; + onPaginate(newPageIndex: number): { newPageIndex: number }; + onReceiveData(domains: CrawlerDomain[], meta: Meta): { domains: CrawlerDomain[]; meta: Meta }; + crawlerDomainDeleted(data: CrawlerData): { data: CrawlerData }; +} + +export const CrawlerDomainsLogic = kea>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawler_domains_logic'], + actions: { + deleteDomain: (domain) => ({ domain }), + fetchCrawlerDomainsData: true, + onReceiveData: (domains, meta) => ({ domains, meta }), + onPaginate: (newPageIndex) => ({ newPageIndex }), + crawlerDomainDeleted: (data) => ({ data }), + }, + reducers: { + dataLoading: [ + true, + { + onReceiveData: () => false, + onPaginate: () => true, + }, + ], + domains: [ + [], + { + onReceiveData: (_, { domains }) => domains, + }, + ], + meta: [ + DEFAULT_META, + { + onReceiveData: (_, { meta }) => meta, + onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex), + }, + ], + }, + listeners: ({ actions, values }) => ({ + fetchCrawlerDomainsData: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const { meta } = values; + + const query = { + 'page[current]': meta.page.current, + 'page[size]': meta.page.size, + }; + + try { + const response = await http.get( + `/internal/app_search/engines/${engineName}/crawler/domains`, + { + query, + } + ); + + const domains = response.results.map(crawlerDomainServerToClient); + + actions.onReceiveData(domains, response.meta); + } catch (e) { + flashAPIErrors(e); + } + }, + + deleteDomain: async ({ domain }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.delete( + `/internal/app_search/engines/${engineName}/crawler/domains/${domain.id}`, + { + query: { + respond_with: 'crawler_details', + }, + } + ); + + const crawlerData = crawlerDataServerToClient(response); + // Publish for other logic files to listen for + actions.crawlerDomainDeleted(crawlerData); + actions.fetchCrawlerDomainsData(); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index 53c980c9750f5..7ba1adb51bbfb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -14,6 +14,7 @@ import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { CrawlerDomainsLogic } from './crawler_domains_logic'; import { CrawlerLogic, CrawlerValues } from './crawler_logic'; import { CrawlerData, @@ -159,6 +160,16 @@ describe('CrawlerLogic', () => { }); describe('listeners', () => { + describe('CrawlerDomainsLogic.actionTypes.crawlerDomainDeleted', () => { + it('updates data in state when a domain is deleted', () => { + jest.spyOn(CrawlerLogic.actions, 'onReceiveCrawlerData'); + CrawlerDomainsLogic.actions.crawlerDomainDeleted(MOCK_CLIENT_CRAWLER_DATA); + expect(CrawlerLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( + MOCK_CLIENT_CRAWLER_DATA + ); + }); + }); + describe('fetchCrawlerData', () => { it('updates logic with data that has been converted from server to client', async () => { jest.spyOn(CrawlerLogic.actions, 'onReceiveCrawlerData'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index d1530c79a6821..08a01af67ece6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -12,6 +12,8 @@ import { flashAPIErrors } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { EngineLogic } from '../engine'; +import { CrawlerDomainsLogic } from './crawler_domains_logic'; + import { CrawlerData, CrawlerDomain, @@ -166,6 +168,9 @@ export const CrawlerLogic = kea>({ actions.onCreateNewTimeout(timeoutIdId); }, + [CrawlerDomainsLogic.actionTypes.crawlerDomainDeleted]: ({ data }) => { + actions.onReceiveCrawlerData(data); + }, }), events: ({ values }) => ({ beforeUnmount: () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts deleted file mode 100644 index a701c43d4775c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts +++ /dev/null @@ -1,93 +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 { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, -} from '../../../__mocks__/kea_logic'; -import '../../__mocks__/engine_logic.mock'; - -jest.mock('./crawler_logic', () => ({ - CrawlerLogic: { - actions: { - onReceiveCrawlerData: jest.fn(), - }, - }, -})); - -import { nextTick } from '@kbn/test/jest'; - -import { CrawlerLogic } from './crawler_logic'; -import { CrawlerOverviewLogic } from './crawler_overview_logic'; - -import { CrawlerDataFromServer, CrawlerDomain } from './types'; -import { crawlerDataServerToClient } from './utils'; - -const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = { - domains: [ - { - id: '507f1f77bcf86cd799439011', - name: 'elastic.co', - created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', - document_count: 13, - sitemaps: [], - entry_points: [], - crawl_rules: [], - deduplication_enabled: false, - deduplication_fields: ['title'], - available_deduplication_fields: ['title', 'description'], - }, - ], - events: [], - most_recent_crawl_request: null, -}; - -const MOCK_CLIENT_CRAWLER_DATA = crawlerDataServerToClient(MOCK_SERVER_CRAWLER_DATA); - -describe('CrawlerOverviewLogic', () => { - const { mount } = new LogicMounter(CrawlerOverviewLogic); - const { http } = mockHttpValues; - const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; - - beforeEach(() => { - jest.clearAllMocks(); - mount(); - }); - - describe('listeners', () => { - describe('deleteDomain', () => { - it('calls onReceiveCrawlerData with retrieved data that has been converted from server to client', async () => { - jest.spyOn(CrawlerLogic.actions, 'onReceiveCrawlerData'); - http.delete.mockReturnValue(Promise.resolve(MOCK_SERVER_CRAWLER_DATA)); - - CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); - await nextTick(); - - expect(http.delete).toHaveBeenCalledWith( - '/internal/app_search/engines/some-engine/crawler/domains/1234', - { - query: { respond_with: 'crawler_details' }, - } - ); - expect(CrawlerLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith( - MOCK_CLIENT_CRAWLER_DATA - ); - expect(flashSuccessToast).toHaveBeenCalled(); - }); - - it('calls flashApiErrors when there is an error', async () => { - http.delete.mockReturnValue(Promise.reject('error')); - - CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts deleted file mode 100644 index 605d45effaa24..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts +++ /dev/null @@ -1,50 +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 { kea, MakeLogicType } from 'kea'; - -import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; - -import { HttpLogic } from '../../../shared/http'; -import { EngineLogic } from '../engine'; - -import { CrawlerLogic } from './crawler_logic'; -import { CrawlerDataFromServer, CrawlerDomain } from './types'; -import { crawlerDataServerToClient, getDeleteDomainSuccessMessage } from './utils'; - -interface CrawlerOverviewActions { - deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain }; -} - -export const CrawlerOverviewLogic = kea>({ - path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'], - actions: { - deleteDomain: (domain) => ({ domain }), - }, - listeners: () => ({ - deleteDomain: async ({ domain }) => { - const { http } = HttpLogic.values; - const { engineName } = EngineLogic.values; - - try { - const response = await http.delete( - `/internal/app_search/engines/${engineName}/crawler/domains/${domain.id}`, - { - query: { - respond_with: 'crawler_details', - }, - } - ); - const crawlerData = crawlerDataServerToClient(response); - CrawlerLogic.actions.onReceiveCrawlerData(crawlerData); - flashSuccessToast(getDeleteDomainSuccessMessage(domain.url)); - } catch (e) { - flashAPIErrors(e); - } - }, - }), -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index beb1e65af47a4..ed445b923ea2a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -28,9 +28,6 @@ const MOCK_VALUES = { domain: { url: 'https://elastic.co', }, - // CrawlerOverviewLogic - domains: [], - crawlRequests: [], }; const MOCK_ACTIONS = { 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 5dff1b934ae5a..01c2ff42fc010 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 @@ -109,6 +109,45 @@ describe('crawler routes', () => { }); }); + describe('GET /internal/app_search/engines/{name}/crawler/domains', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/internal/app_search/engines/{name}/crawler/domains', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/domains', + }); + }); + + it('validates correctly', () => { + const request = { + params: { name: 'some-engine' }, + query: { + 'page[current]': 5, + 'page[size]': 10, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without required params', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + }); + describe('POST /internal/app_search/engines/{name}/crawler/crawl_requests/cancel', () => { 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 72a48a013636c..9336d9ac93e70 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 @@ -69,6 +69,24 @@ export function registerCrawlerRoutes({ }) ); + router.get( + { + path: '/internal/app_search/engines/{name}/crawler/domains', + validate: { + params: schema.object({ + name: schema.string(), + }), + query: schema.object({ + 'page[current]': schema.number(), + 'page[size]': schema.number(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/domains', + }) + ); + router.post( { path: '/internal/app_search/engines/{name}/crawler/domains', From 8819bd8faed63ee4f1dfe21e2036fc609dca8de0 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Tue, 9 Nov 2021 14:48:12 +0100 Subject: [PATCH 31/98] [Upgrade Assistant] Forwardport from 7.x (#114966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix link to Cloud deployment URL in upgrade step. (#109528) * [Upgrade Assistant] Refactor CITs * Rename UA steps to fix_issues_step and fix_logs_step. (#109526) * Rename tests accordingly. * [Upgrade Assistant] Cleanup scss (#109524) * [Upgrade Assistant] Update readme (#109502) * Add "Back up data" step to UA (#109543) * Add backup step with static content and link to Snapshot and Restore. * Add snapshot_restore locator. * Remove unnecessary describe block from Upgrade Step tests. * Remove unused render_app.tsx. * Change copy references of 'deprecation issues' to 'deprecation warnings'. (#109963) * [Upgrade Assistant] Address design feedback for ES deprecations page (#109726) * [Upgrade Assistant] Add checkpoint feature to Overview page (#109449) * Add on-Cloud state to Upgrade Assistant 'Back up data' step (#109956) * [Upgrade Assistant] Refactor external links to use locators (#110435) * [Upgrade Assistant] Use AppContext for services instead of KibanaContext (#109801) * Remove kibana context dependency in favour of app context * Add missing type to ContextValue * Fix mock type * Refactor app mount flow and types * Refactor to use useServices hook * Fix linter issues * Keep mount_management_section and initialize breadcrumbs and api there * Remove useServices and usePlugins in favour of just useAppContext * Remove unnecessary mocks * [Upgrade Assistant] Enable functional and a11y tests (#109909) * [Upgrade Assistant] Remove version from UA nav title (#110739) * [Upgrade Assistant] New Kibana deprecations page (#110101) * Use injected lib.handleEsError instead of importing it in Upgrade Assistant API route handlers. (#111067) * Add tests for UA back up data step on Cloud (#111066) * Update UA to consume snapshotsUrl as provided by the Cloud plugin. (#111239) * Skip flaky UA Backup step polling test. * [Upgrade Assistant] Refactor kibana deprecation service mocks (#111168) * [Upgrade Assistant] Remove unnecessary EuiScreenReaderOnly from stat panels (#111518) * Remove EuiScreenReaderOnly implementations * Remove unused translations * Remove extra string after merge conflict * Use consistent 'issues' and 'critical' vs. 'warning' terminology in UA. (#111221) * Refactor UA Overview to support step-completion (#111243) * Refactor UA Overview to store step-completion state at the root and delegate step-completion logic to each step component. * Add completion status to logs and issues steps * [Upgrade Assistant] External links with checkpoint time-range applied (#111252) * Bound query around last checkpoint date * Fix tests * Also test discover url contains search params * Small refactor * Keep state about lastCheckpoint in parent component * Remove space * Address CR changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Upgrade Assistant] Tests for updating step state accordingly if API poll receives count followed by error (#111701) * Add test for logs count polling * Test when count api fails * [Upgrade Assistant] Add a11y tests for es deprecation flyouts (#110843) * [Upgrade Assistant] Set fix_logs step as incomplete if log collection is not enabled (#111827) * set step as incomplete if toggle is disabled * Fix test names * Remove unnecessary mocks * [Upgrade Assistant] Update copy to use "issues" instead of "warnings" (#111817) * Create common deprecation issues panel component in UA (#111231) * Refine success state behavior and add tests. * Refactor components into a components directory. * Refactor SCSS to colocate styles with their components. * Refactor tests to reduce boilerplate and clarify conditions under test. * [Upgrade Assistant] Fix Kibana deprecations warning message * [Upgrade Assistant] Add support for API keys when reindexing (#111451) * [Upgrade Assistant] Update readme (#112154) * [Upgrade Assistant] Make infra plugin optional (#111960) * Make infra plugin optional * Fix CR requests * [Upgrade Assistant] Improve flyout information architecture (#111713) * Make sure longstrings inside flyout body are text-wrap * Show resolved badge for reindex flyout and row * Finish off rest of ES deprecation flyouts * Refactor deprecation badge into its own component * Add tests for kibana deprecations * Add tests for es deprecations * Also check that we have status=error before rendering error callout * Check for non-complete states instead of just error * Small refactor * Default deprecation is not resolvable * Add a bit more spacing between title and badge * Address CR changes * Use EuiSpacer instead of flexitems * [Upgrade Assistant] Update readme (#112195) * [Upgrade Assistant] Add integration tests for Overview page (#111370) * Add a11y tests for when overview page has toggle enabled * Add functional and accessibility tests for overview page * Load test files * Fix linter error * Navigate before asserting * Steps have now completion state * Remove duped word * Run setup only once, not per test * Address CR changes * No need to renavigate to the page Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Upgrade Assistant] Add note about compatibility headers (#110469) * Improve error states for Upgrade Assistant deprecation issues (#112457) * Simplify error state for deprecation issues panels. Remove . * Rename components from stats -> panel. * Create common error-reporting component for use in both Kibana and ES deprecations pages. * Align order of loading, error, and success states between these pages. * Change references to 'deprecations' -> 'deprecation issues'. * Fix tests for panels. * Add API integration test for handling auth error. * Fix TS errors. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * Change count poll time to 15s (#112669) * [Upgrade Assistant] Add permissions check to logs step (#112420) * [Upgrade Assistant] Refactor telemetry (#112177) * [Upgrade Assistant] Check for ML upgrade mode before enabling flyout actions (#112555) * Add missing error handlers for deprecation logging route (#113109) * [Upgrade Assistant] Batch reindex docs (#112960) * [UA] Added batch reindexing docs link to the ES deprecations page. Added a link from "batch reindexing" docs page to "start or resume reindex" docs page and from there to ES reindexing docs page. Also renamed "reindexing operation" to "reindexing task" for consistency. * [Upgrade Assistant] Added docs build files * Update x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> * Update x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> * [Upgrade Assistant] Added review suggestions and fixed eslint issues Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Upgrade Assistant] Improve error messages for GET /api/upgrade_assistant/reindex/ (#112961) * Add support for single manual steps to Upgrade Assistant. (#113344) * Revert "[Upgrade Assistant] Refactor telemetry (#112177)" (#113665) This reverts commit 991d24bad21ccf4b8350cba2b2ed3ceca6d90cea. * [Upgrade Assistant] Use skipFetchFields when creating the indexPattern in order to avoid errors if index doesn't exist (#113821) * Use skipFetchFields when creating the indexPatter in order to avoid errors when index doesnt exist * Address CR feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Upgrade Assistant] Hide system indices from es deprecations list (#113627) * Refactor reindex routes into separate single and batch reindex files. Apply version precheck to batch routes. (#113822) * [Upgrade Assistant] Remove ML/Watcher logic (#113224) * Add show upgrade flag to url (#114243) * [Upgrade Assistant] Delete deprecation log cache (#114113) * [Upgrade Assistant] Add upgrade system indices section (#110593) * [Upgrade Assistant] Reindexing progress (#114275) * [Upgrade Assistant] Added reindexing progress in % to the reindex flyout and es deprecations table * [Upgrade Assistant] Renamed first argument in `getReindexProgressLabel` to `reindexTaskPercComplete` for consistency Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Upgrade Assistant] Remove Fix manually heading when there are no manual steps * Add rolling upgrade interstitials to UA (#112907) * Refactor FixLogsStep to be explicit in which props are passed to DeprecationLoggingToggle. * Centralize error-handling logic in the api service, instead of handling it within each individual API request. Covers: - Cloud backup status - ES deprecations - Deprecation logging - Remove index settings - ML - Reindexing Also: - Handle 426 error state and surface in UI. - Move ResponseError type into common/types. * Add note about intended use case of status API route. * Add endpoint dedicated to surfacing the cluster upgrade state, and a client-side poll. * Merge App and AppWithRouter components. * [Upgrade Assistant] Added "accept changes" header to the warnings list in the reindex flyout (#114798) * Refactor kibana deprecation tests (#114763) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * Fix linter issues * Remove unused translation * Prefer master changes over 7.x for ml docs * Prefer master changes over 7.x * Skip tests * Move everything to a single describe * Fix types * Add missing prop to mock * [Upgrade Assistant] Removed "closed index" warning from reindex flyout (#114861) * [Upgrade Assistant] Removed "closed index" warning that reindexing might take longer than usual, which is not the case * [Upgrade Assistant] Also deleted i18n strings that are not needed anymore * Add LevelIconTips to be more explicit about the difference between critical and warning issues. (#115121) * Extract common DeprecationFlyoutLearnMoreLink component and change wording to 'Learn more'. (#115117) * [Upgrade Assistant] Reindexing cancellation (#114636) * [Upgrade Assistant] Updated the reindexing cancellation to look less like an error * [Upgrade Assistant] Fixed an i18n issue and updated a jest snapshot * [Upgrade Assistant] Updated cancelled reindexing state with a unified label and cross icon * [Upgrade Assistant] Fixed snapshot test * [Upgrade Assistant] Updated spacing to the reindex cancel button Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * Fix test errors (#115183) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Upgrade Assistant] Overview page UI clean up (#115258) - Scaling down deprecation issue panel title size to flow with typographic hierarchy. - Removing panel around deprecation logging switch to reduce visual elements. - Using success instead of green color for migration complete message. * Revert "Revert "[Upgrade Assistant] Refactor telemetry (#112177)" (#113665)" (#114804) This reverts commit c385d498874d4ca34f454e3cb9ad9e4c0e3219ae. * Add migration to remove obsolete attributes from telemetry saved object. * Refactor UA telemetry constants by extracting it from common/types. * [Upgrade Assistant] Rename upgrade_status to migration_status (#114755) * [Upgrade Assistant] Swapped reindexing flyouts order (#115046) * [Upgrade Assistant] Changed reindexing steps order, replaced a warning callout with a text element * [Upgrade Assistant] Fixed reindex flyout test and changed warning callout from danger color to warning color * [Upgrade Assistant] Fixed the correct status to show warnings * [Upgrade Assistant] Fixed i18n strings * [Upgrade Assistant] Moved reindex with warnings logic into a function * [Upgrade Assistant] Updated reindex flyout copy * [Upgrade Assistant] Also added a trailing period to the reindex step 3 * [Upgrade Assistant] Fixed i18n strings and step 3 wording * [Upgrade Assistant] Added docs changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Upgrade Assistant] Hide features that don't need to be migrated from flyout (#115535) * Filter out system indices that dont require migration on server side * Rename to attrs to migration * Update flyout snapshot. * Refine Upgrade Assistant copy. (#115472) * Remove unused file * Fix kibanaVersion dep * Updated config.ts to fix UA test UA functional API integration test to check cloud backup status creates a snapshot repo, which fails to be created with my changes to config.ts `'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2,'`. Adding `/tmp/cloud-snapshots/'` to the config fixes the test. * Address CR changes * Add missing error handler for system indices migration (#116088) * Fix broken tests * Fix test * Skip tests * Fix linter errors and import * [Upgrade Assistant] Fix typo in retrieval of cluster settings (#116335) * Fix typos * Fix typo also in server tests * Make sure log collection remains enabled throughout the test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * Fix type errors * Fix integration test types * Fix accessibility test type errors * Fix linter errors in shared_imports * Fix functional test types Co-authored-by: CJ Cenizal Co-authored-by: Alison Goryachev Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Yulia Čech <6585477+yuliacech@users.noreply.github.com> Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> Co-authored-by: Dmitry Borodyansky --- .../batch_reindexing.asciidoc | 4 +- .../upgrade-assistant/cancel_reindex.asciidoc | 2 +- .../check_reindex_status.asciidoc | 8 +- .../api/upgrade-assistant/reindexing.asciidoc | 17 +- docs/api/upgrade-assistant/status.asciidoc | 2 +- docs/developer/plugin-list.asciidoc | 3 +- ...-plugin-core-public.doclinksstart.links.md | 9 +- .../public/doc_links/doc_links_service.ts | 23 +- src/core/public/public.api.md | 9 +- .../public/request/use_request.ts | 9 +- .../schema/xpack_plugins.json | 38 -- .../translations/translations/ja-JP.json | 59 -- .../translations/translations/zh-CN.json | 61 -- x-pack/plugins/upgrade_assistant/README.md | 273 ++++++-- .../client_integration/app/app.helpers.tsx | 50 ++ .../app/cluster_upgrade.test.tsx | 86 +++ .../default_deprecation_flyout.test.ts | 11 +- .../es_deprecations/deprecations_list.test.ts | 55 +- .../es_deprecations/error_handling.test.ts | 37 +- .../es_deprecations.helpers.ts | 161 +++++ .../index_settings_deprecation_flyout.test.ts | 29 +- .../ml_snapshots_deprecation_flyout.test.ts | 64 +- .../reindex_deprecation_flyout.test.ts | 159 ++++- .../helpers/app_context.mock.ts | 86 +++ .../helpers/elasticsearch.helpers.ts | 171 ----- .../helpers/http_requests.ts | 121 +++- .../client_integration/helpers/index.ts | 10 +- .../helpers/kibana.helpers.ts | 59 -- .../helpers/services_mock.ts | 30 - .../helpers/setup_environment.tsx | 55 +- .../helpers/time_manipulation.ts | 24 + .../client_integration/kibana.test.ts | 232 ------- .../deprecation_details_flyout.test.ts | 161 +++++ .../deprecations_table.test.ts | 127 ++++ .../deprecations_table/error_handling.test.ts | 101 +++ .../kibana_deprecations.helpers.ts | 127 ++++ .../kibana_deprecations/service.mock.ts | 110 ++++ .../overview/backup_step/backup_step.test.tsx | 176 ++++++ .../fix_deprecation_logs_step.test.tsx | 153 ----- .../elasticsearch_deprecation_issues.test.tsx | 193 ++++++ .../fix_issues_step/fix_issues_step.test.tsx | 75 +++ .../kibana_deprecation_issues.test.tsx | 133 ++++ .../mock_es_issues.ts} | 37 +- .../fix_logs_step/fix_logs_step.test.tsx | 473 ++++++++++++++ .../__snapshots__/flyout.test.ts.snap | 22 + .../migrate_system_indices/flyout.test.ts | 42 ++ .../migrate_system_indices.test.tsx | 167 +++++ .../overview/migrate_system_indices/mocks.ts | 58 ++ .../step_completion.test.ts | 86 +++ .../{helpers => overview}/overview.helpers.ts | 80 ++- .../overview/overview.test.tsx | 8 +- .../review_logs_step.test.tsx | 233 ------- .../upgrade_step/upgrade_step.test.tsx | 23 +- .../upgrade_assistant/common/constants.ts | 14 + .../plugins/upgrade_assistant/common/types.ts | 84 +-- x-pack/plugins/upgrade_assistant/kibana.json | 6 +- .../public/application/_index.scss | 1 - .../public/application/app.tsx | 172 +++-- .../public/application/app_context.tsx | 32 +- .../public/application/components/_index.scss | 2 - .../components/coming_soon_prompt.tsx | 8 +- .../application/components/constants.tsx | 27 +- .../components/es_deprecations/_index.scss | 1 - .../deprecation_types/_index.scss | 1 - .../deprecation_types/default/flyout.tsx | 19 +- .../deprecation_types/default/table_row.tsx | 3 +- .../index_settings/flyout.tsx | 43 +- .../index_settings/resolution_table_cell.tsx | 2 +- .../index_settings/table_row.tsx | 8 +- .../ml_snapshots/context.tsx | 4 + .../deprecation_types/ml_snapshots/flyout.tsx | 136 +++- .../ml_snapshots/resolution_table_cell.tsx | 2 +- .../ml_snapshots/table_row.tsx | 7 +- .../ml_snapshots/use_snapshot_state.tsx | 7 +- .../deprecation_types/reindex/_index.scss | 1 - .../checklist_step.test.tsx.snap | 45 +- .../__snapshots__/warning_step.test.tsx.snap | 31 +- .../reindex/flyout/_index.scss | 1 - .../reindex/flyout/_step_progress.scss | 2 +- .../reindex/flyout/checklist_step.test.tsx | 51 +- .../reindex/flyout/checklist_step.tsx | 143 +++-- .../reindex/flyout/container.tsx | 154 ++--- .../reindex/flyout/progress.test.tsx | 201 ++---- .../reindex/flyout/progress.tsx | 253 ++++---- .../reindex/flyout/step_progress.tsx | 10 +- .../reindex/flyout/warning_step.test.tsx | 21 +- .../reindex/flyout/warning_step_checkbox.tsx | 2 +- .../reindex/flyout/warnings_step.tsx | 104 ++-- .../reindex/resolution_table_cell.tsx | 27 +- .../deprecation_types/reindex/table_row.tsx | 24 +- .../reindex/use_reindex_state.tsx | 116 ++-- .../es_deprecations/es_deprecation_errors.tsx | 50 -- .../es_deprecations/es_deprecations.tsx | 117 +++- .../es_deprecations/es_deprecations_table.tsx | 31 +- .../es_deprecations_table_cells.tsx | 29 +- .../public/application/components/index.ts | 11 + .../_deprecation_details_flyout.scss | 4 + .../deprecation_details_flyout.tsx | 242 ++++++++ .../kibana_deprecations/deprecation_item.tsx | 145 ----- .../kibana_deprecations/deprecation_list.tsx | 150 ----- .../components/kibana_deprecations/index.ts | 2 +- .../kibana_deprecation_errors.tsx | 73 --- .../kibana_deprecations.tsx | 308 +++++---- .../kibana_deprecations_table.tsx | 235 +++++++ .../resolution_table_cell.tsx | 145 +++++ .../resolve_deprecation_modal.tsx | 64 -- .../kibana_deprecations/steps_modal.tsx | 115 ---- .../components/overview/_index.scss | 2 - .../overview/backup_step/backup_step.tsx | 45 ++ .../overview/backup_step/cloud_backup.tsx | 152 +++++ .../backup_step}/index.ts | 2 +- .../overview/backup_step/on_prem_backup.tsx | 59 ++ .../fix_deprecation_logs_step/_index.scss | 1 - .../external_links.tsx | 120 ---- .../fix_deprecation_logs_step.tsx | 90 --- .../components/_deprecation_issues_panel.scss | 24 + .../components/deprecation_issues_panel.tsx | 135 ++++ .../es_deprecation_issues_panel.tsx | 46 ++ .../fix_issues_step/components/index.ts | 9 + .../kibana_deprecation_issues_panel.tsx | 73 +++ .../components/loading_issues_error.tsx | 21 + .../components/no_deprecation_issues.tsx | 45 ++ .../fix_issues_step/fix_issues_step.tsx | 79 +++ .../index.ts | 2 +- .../_deprecation_logging_toggle.scss | 0 .../deprecation_logging_toggle.tsx | 21 +- .../deprecation_logging_toggle/index.ts | 0 .../deprecations_count_checkpoint.tsx | 149 +++++ .../deprecations_count_checkpoint}/index.ts | 2 +- .../fix_logs_step/external_links.test.ts | 49 ++ .../overview/fix_logs_step/external_links.tsx | 175 ++++++ .../overview/fix_logs_step/fix_logs_step.tsx | 235 +++++++ .../es_stats => fix_logs_step}/index.ts | 2 +- .../use_deprecation_logging.ts | 9 +- .../migrate_system_indices/flyout.tsx | 193 ++++++ .../index.ts | 2 +- .../migrate_system_indices.tsx | 239 +++++++ .../use_migrate_system_indices.ts | 93 +++ .../components/overview/overview.tsx | 76 ++- .../overview/review_logs_step/_index.scss | 2 - .../review_logs_step/_stats_panel.scss | 6 - .../review_logs_step/es_stats/es_stats.tsx | 146 ----- .../es_stats/es_stats_error.tsx | 79 --- .../kibana_stats/kibana_stats.tsx | 188 ------ .../no_deprecations/_no_deprecations.scss | 3 - .../no_deprecations/no_deprecations.tsx | 35 -- .../review_logs_step/review_logs_step.tsx | 53 -- .../overview/upgrade_step/upgrade_step.tsx | 46 +- .../components/shared/deprecation_badge.tsx | 51 ++ .../components/shared/deprecation_count.tsx | 71 +++ .../deprecation_flyout_learn_more_link.tsx | 24 + .../deprecation_list_bar/count_summary.tsx | 41 -- .../deprecation_list_bar.tsx | 69 -- .../shared/deprecation_list_bar/index.ts | 8 - .../shared/deprecation_pagination.tsx | 24 - .../deprecations_page_loading_error.tsx | 42 ++ .../application/components/shared/health.tsx | 88 --- .../application/components/shared/index.ts | 9 +- .../components/shared/level_info_tip.tsx | 27 + .../group_by_filter.test.tsx.snap | 24 - .../__snapshots__/level_filter.test.tsx.snap | 19 - .../search_bar/group_by_filter.test.tsx | 31 - .../shared/search_bar/group_by_filter.tsx | 54 -- .../shared/search_bar/level_filter.test.tsx | 34 - .../shared/search_bar/level_filter.tsx | 51 -- .../shared/search_bar/search_bar.tsx | 141 ----- .../public/application/components/types.ts | 18 +- .../public/application/lib/api.ts | 150 +++-- .../public/application/lib/breadcrumbs.ts | 4 +- .../lib/get_es_deprecation_error.ts | 6 +- .../public/application/lib/logs_checkpoint.ts | 30 + .../public/application/lib/ui_metric.ts | 49 ++ .../public/application/lib/utils.test.ts | 33 +- .../public/application/lib/utils.ts | 48 ++ .../application/mount_management_section.ts | 48 -- .../application/mount_management_section.tsx | 32 + .../public/application/render_app.tsx | 22 - .../upgrade_assistant/public/index.scss | 1 - .../plugins/upgrade_assistant/public/index.ts | 1 - .../upgrade_assistant/public/plugin.ts | 53 +- .../public/shared_imports.ts | 18 +- .../plugins/upgrade_assistant/public/types.ts | 36 +- .../lib/__fixtures__/fake_deprecations.json | 9 + .../lib/es_deprecation_logging_apis.test.ts | 12 +- .../server/lib/es_deprecation_logging_apis.ts | 4 +- .../server/lib/es_deprecations_status.test.ts | 45 ++ .../server/lib/es_deprecations_status.ts | 15 +- .../lib/es_system_indices_migration.test.ts | 52 ++ .../server/lib/es_system_indices_migration.ts | 51 ++ .../lib/reindexing/credential_store.test.ts | 180 +++++- .../server/lib/reindexing/credential_store.ts | 140 ++++- .../lib/reindexing/reindex_actions.test.ts | 43 -- .../server/lib/reindexing/reindex_actions.ts | 109 +--- .../lib/reindexing/reindex_service.test.ts | 587 +----------------- .../server/lib/reindexing/reindex_service.ts | 175 +----- .../server/lib/reindexing/worker.ts | 15 +- .../lib/telemetry/es_ui_open_apis.test.ts | 48 -- .../server/lib/telemetry/es_ui_open_apis.ts | 57 -- .../lib/telemetry/es_ui_reindex_apis.test.ts | 52 -- .../lib/telemetry/es_ui_reindex_apis.ts | 63 -- .../lib/telemetry/usage_collector.test.ts | 31 - .../server/lib/telemetry/usage_collector.ts | 113 +--- .../upgrade_assistant/server/plugin.ts | 34 +- .../server/routes/__mocks__/request.mock.ts | 1 + .../upgrade_assistant/server/routes/app.ts | 80 +++ .../server/routes/cloud_backup_status.ts | 54 ++ .../server/routes/cluster_upgrade_status.ts | 21 + .../server/routes/deprecation_logging.test.ts | 109 +++- .../server/routes/deprecation_logging.ts | 104 +++- .../server/routes/es_deprecations.test.ts | 3 + .../server/routes/es_deprecations.ts | 12 +- .../server/routes/ml_snapshots.test.ts | 25 + .../server/routes/ml_snapshots.ts | 38 +- .../server/routes/register_routes.ts | 13 +- .../batch_reindex_indices.test.ts | 192 ++++++ .../reindex_indices/batch_reindex_indices.ts | 133 ++++ .../reindex_indices/create_reindex_worker.ts | 38 ++ .../server/routes/reindex_indices/index.ts | 4 +- .../map_any_error_to_kibana_http_response.ts | 45 ++ .../routes/reindex_indices/reindex_handler.ts | 16 +- .../reindex_indices/reindex_indices.test.ts | 138 +--- .../routes/reindex_indices/reindex_indices.ts | 199 +----- .../server/routes/status.test.ts | 7 +- .../upgrade_assistant/server/routes/status.ts | 14 +- .../routes/system_indices_migration.test.ts | 148 +++++ .../server/routes/system_indices_migration.ts | 76 +++ .../server/routes/telemetry.test.ts | 187 ------ .../server/routes/telemetry.ts | 64 -- .../saved_object_types/migrations}/index.ts | 2 +- .../telemetry_saved_object_migrations.test.ts | 41 ++ .../telemetry_saved_object_migrations.ts | 40 ++ .../telemetry_saved_object_type.ts | 42 +- .../server/shared_imports.ts | 1 + .../plugins/upgrade_assistant/server/types.ts | 11 +- .../plugins/upgrade_assistant/tsconfig.json | 1 + .../accessibility/apps/upgrade_assistant.ts | 215 +++++-- .../upgrade_assistant/cloud_backup_status.ts | 93 +++ .../apis/upgrade_assistant/es_deprecations.ts | 41 ++ .../apis/upgrade_assistant/index.ts | 3 + .../apis/upgrade_assistant/privileges.ts | 58 ++ x-pack/test/api_integration/config.ts | 2 +- .../upgrade_assistant/deprecation_pages.ts | 105 ++++ .../upgrade_assistant_security.ts | 6 - .../apps/upgrade_assistant/index.ts | 5 +- .../apps/upgrade_assistant/overview_page.ts | 77 +++ .../upgrade_assistant/upgrade_assistant.ts | 79 --- .../page_objects/upgrade_assistant_page.ts | 67 +- 247 files changed, 10101 insertions(+), 6437 deletions(-) create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/app.helpers.tsx create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/cluster_upgrade.test.tsx create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/es_deprecations.helpers.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts delete mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/elasticsearch.helpers.ts delete mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/kibana.helpers.ts delete mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/services_mock.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts delete mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.test.tsx create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx rename x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/{review_logs_step/mocked_responses.ts => fix_issues_step/mock_es_issues.ts} (66%) create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts rename x-pack/plugins/upgrade_assistant/__jest__/client_integration/{helpers => overview}/overview.helpers.ts (50%) delete mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/review_logs_step.test.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/_index.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/_index.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/_index.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/_index.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_index.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/index.ts create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/_deprecation_details_flyout.scss create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx rename x-pack/plugins/upgrade_assistant/public/application/components/{shared/search_bar => overview/backup_step}/index.ts (84%) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/_index.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/external_links.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/_deprecation_issues_panel.scss create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/deprecation_issues_panel.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/loading_issues_error.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/no_deprecation_issues.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx rename x-pack/plugins/upgrade_assistant/public/application/components/overview/{review_logs_step/no_deprecations => fix_issues_step}/index.ts (82%) rename x-pack/plugins/upgrade_assistant/public/application/components/overview/{fix_deprecation_logs_step => fix_logs_step}/deprecation_logging_toggle/_deprecation_logging_toggle.scss (100%) rename x-pack/plugins/upgrade_assistant/public/application/components/overview/{fix_deprecation_logs_step => fix_logs_step}/deprecation_logging_toggle/deprecation_logging_toggle.tsx (89%) rename x-pack/plugins/upgrade_assistant/public/application/components/overview/{fix_deprecation_logs_step => fix_logs_step}/deprecation_logging_toggle/index.ts (100%) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx rename x-pack/plugins/upgrade_assistant/public/application/components/overview/{fix_deprecation_logs_step => fix_logs_step/deprecations_count_checkpoint}/index.ts (76%) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx rename x-pack/plugins/upgrade_assistant/public/application/components/overview/{review_logs_step/es_stats => fix_logs_step}/index.ts (83%) rename x-pack/plugins/upgrade_assistant/public/application/components/overview/{fix_deprecation_logs_step => fix_logs_step}/use_deprecation_logging.ts (94%) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx rename x-pack/plugins/upgrade_assistant/public/application/components/overview/{review_logs_step => migrate_system_indices}/index.ts (77%) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_index.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_stats_panel.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats_error.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/kibana_stats.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/_no_deprecations.scss delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/no_deprecations.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/review_logs_step.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_badge.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx create mode 100644 x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts create mode 100644 x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts create mode 100644 x-pack/plugins/upgrade_assistant/public/application/mount_management_section.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/application/render_app.tsx delete mode 100644 x-pack/plugins/upgrade_assistant/public/index.scss create mode 100644 x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts delete mode 100644 x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts delete mode 100644 x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts delete mode 100644 x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts delete mode 100644 x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/app.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/cluster_upgrade_status.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/map_any_error_to_kibana_http_response.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts delete mode 100644 x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts delete mode 100644 x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts rename x-pack/plugins/upgrade_assistant/{public/application/components/overview/review_logs_step/kibana_stats => server/saved_object_types/migrations}/index.ts (74%) create mode 100644 x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.test.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.ts create mode 100644 x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts create mode 100644 x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts create mode 100644 x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts create mode 100644 x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts create mode 100644 x-pack/test/functional/apps/upgrade_assistant/overview_page.ts delete mode 100644 x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts diff --git a/docs/api/upgrade-assistant/batch_reindexing.asciidoc b/docs/api/upgrade-assistant/batch_reindexing.asciidoc index db3e080d09185..6b355185de5ce 100644 --- a/docs/api/upgrade-assistant/batch_reindexing.asciidoc +++ b/docs/api/upgrade-assistant/batch_reindexing.asciidoc @@ -6,7 +6,7 @@ experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] -Start or resume multiple reindexing tasks in one request. Additionally, reindexing tasks started or resumed +Start or resume multiple <> tasks in one request. Additionally, reindexing tasks started or resumed via the batch endpoint will be placed on a queue and executed one-by-one, which ensures that minimal cluster resources are consumed over time. @@ -76,7 +76,7 @@ Similar to the <>, the API retur } -------------------------------------------------- -<1> A list of reindex operations created, the order in the array indicates the order in which tasks will be executed. +<1> A list of reindex tasks created, the order in the array indicates the order in which tasks will be executed. <2> Presence of this key indicates that the reindex job will occur in the batch. <3> A Unix timestamp of when the reindex task was placed in the queue. <4> A list of errors that may have occurred preventing the reindex task from being created. diff --git a/docs/api/upgrade-assistant/cancel_reindex.asciidoc b/docs/api/upgrade-assistant/cancel_reindex.asciidoc index 04ab3bdde35fc..93e4c6fda6b40 100644 --- a/docs/api/upgrade-assistant/cancel_reindex.asciidoc +++ b/docs/api/upgrade-assistant/cancel_reindex.asciidoc @@ -4,7 +4,7 @@ Cancel reindex ++++ -experimental[] Cancel reindexes that are waiting for the {es} reindex task to complete. For example, `lastCompletedStep` set to `40`. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] Cancel reindexes that are waiting for the Elasticsearch reindex task to complete. For example, `lastCompletedStep` set to `40`. diff --git a/docs/api/upgrade-assistant/check_reindex_status.asciidoc b/docs/api/upgrade-assistant/check_reindex_status.asciidoc index 75aac7b3699f5..934fd92312b04 100644 --- a/docs/api/upgrade-assistant/check_reindex_status.asciidoc +++ b/docs/api/upgrade-assistant/check_reindex_status.asciidoc @@ -4,7 +4,9 @@ Check reindex status ++++ -experimental[] Check the status of the reindex operation. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] + +Check the status of the reindex task. [[check-reindex-status-request]] ==== Request @@ -43,7 +45,7 @@ The API returns the following: <2> Current status of the reindex. For details, see <>. <3> Last successfully completed step of the reindex. For details, see <> table. <4> Task ID of the reindex task in Elasticsearch. Only present if reindexing has started. -<5> Percentage of how far the reindexing task in Elasticsearch has progressed, in decimal from from 0 to 1. +<5> Percentage of how far the reindexing task in Elasticsearch has progressed, in decimal form from 0 to 1. <6> Error that caused the reindex to fail, if it failed. <7> An array of any warning codes explaining what changes are required for this reindex. For details, see <>. <8> Specifies if the user has sufficient privileges to reindex this index. When security is unavailable or disables, returns `true`. @@ -73,7 +75,7 @@ To resume the reindex, you must submit a new POST request to the `/api/upgrade_a ==== Step codes `0`:: - The reindex operation has been created in Kibana. + The reindex task has been created in Kibana. `10`:: The index group services stopped. Only applies to some system indices. diff --git a/docs/api/upgrade-assistant/reindexing.asciidoc b/docs/api/upgrade-assistant/reindexing.asciidoc index ce5670822e5ad..ccb9433ac24b1 100644 --- a/docs/api/upgrade-assistant/reindexing.asciidoc +++ b/docs/api/upgrade-assistant/reindexing.asciidoc @@ -4,9 +4,18 @@ Start or resume reindex ++++ -experimental[] Start a new reindex or resume a paused reindex. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] + +Start a new reindex or resume a paused reindex. Following steps are performed during +a reindex task: + +. Setting the index to read-only +. Creating a new index +. {ref}/docs-reindex.html[Reindexing] documents into the new index +. Creating an index alias for the new index +. Deleting the old index + -Start a new reindex or resume a paused reindex. [[start-resume-reindex-request]] ==== Request @@ -40,6 +49,6 @@ The API returns the following: <1> The name of the new index. <2> The reindex status. For more information, refer to <>. <3> The last successfully completed step of the reindex. For more information, refer to <>. -<4> The task ID of the reindex task in {es}. Appears when the reindexing starts. -<5> The progress of the reindexing task in {es}. Appears in decimal form, from 0 to 1. +<4> The task ID of the {ref}/docs-reindex.html[reindex] task in {es}. Appears when the reindexing starts. +<5> The progress of the {ref}/docs-reindex.html[reindexing] task in {es}. Appears in decimal form, from 0 to 1. <6> The error that caused the reindex to fail, if it failed. diff --git a/docs/api/upgrade-assistant/status.asciidoc b/docs/api/upgrade-assistant/status.asciidoc index 42030061c4289..b0c11939ca784 100644 --- a/docs/api/upgrade-assistant/status.asciidoc +++ b/docs/api/upgrade-assistant/status.asciidoc @@ -4,7 +4,7 @@ Upgrade readiness status ++++ -experimental[] Check the status of your cluster. +experimental["The underlying Upgrade Assistant concepts are stable, but the APIs for managing Upgrade Assistant are experimental."] Check the status of your cluster. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index de679692e7a84..1429ad29be5fd 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -600,8 +600,7 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona |{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant/README.md[upgradeAssistant] -|Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary -purposes are to: +|Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. It will only be enabled on the last minor before the next major release. This is controlled via the config: xpack.upgrade_assistant.readonly (#101296). |{kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 676f7420c8bb9..5882543c6496e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -10,6 +10,9 @@ readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -133,7 +136,11 @@ readonly links: { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index c54922ecd67df..c459f96d402f5 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -22,6 +22,7 @@ export class DocLinksService { // Documentation for `main` branches is still published at a `master` URL. const DOC_LINK_VERSION = kibanaBranch === 'main' ? 'master' : kibanaBranch; const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; + const STACK_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack/${DOC_LINK_VERSION}/`; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`; const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`; @@ -36,6 +37,9 @@ export class DocLinksService { links: { settings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/settings.html`, elasticStackGetStarted: `${STACK_GETTING_STARTED}get-started-elastic-stack.html`, + upgrade: { + upgradingElasticStack: `${STACK_DOCS}upgrading-elastic-stack.html`, + }, apm: { kibanaSettings: `${KIBANA_DOCS}apm-settings-in-kibana.html`, supportedServiceMaps: `${KIBANA_DOCS}service-maps.html#service-maps-supported`, @@ -158,7 +162,11 @@ export class DocLinksService { }, addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, kibana: `${KIBANA_DOCS}index.html`, - upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`, + upgradeAssistant: { + overview: `${KIBANA_DOCS}upgrade-assistant.html`, + batchReindex: `${KIBANA_DOCS}batch-start-resume-reindex.html`, + remoteReindex: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-from-remote`, + }, rollupJobs: `${KIBANA_DOCS}data-rollups.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, @@ -222,10 +230,11 @@ export class DocLinksService { remoteClustersProxy: `${ELASTICSEARCH_DOCS}remote-clusters.html#proxy-mode`, remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}remote-clusters-settings.html#remote-cluster-proxy-settings`, scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`, - setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, shardAllocationSettings: `${ELASTICSEARCH_DOCS}modules-cluster.html#cluster-shard-allocation-settings`, transportSettings: `${ELASTICSEARCH_DOCS}modules-network.html#common-network-settings`, typesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, + setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, + apiCompatibilityHeader: `${ELASTICSEARCH_DOCS}api-conventions.html#api-compatibility`, }, siem: { guide: `${SECURITY_SOLUTION_DOCS}index.html`, @@ -289,6 +298,7 @@ export class DocLinksService { outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-regression.html#ml-dfanalytics-regression-evaluation`, classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-class-aucroc`, + setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, @@ -522,6 +532,9 @@ export interface DocLinksStart { readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -645,7 +658,11 @@ export interface DocLinksStart { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 83aea9774bb56..4eea3e1e475cf 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -478,6 +478,9 @@ export interface DocLinksStart { readonly links: { readonly settings: string; readonly elasticStackGetStarted: string; + readonly upgrade: { + readonly upgradingElasticStack: string; + }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; @@ -601,7 +604,11 @@ export interface DocLinksStart { }; readonly addData: string; readonly kibana: string; - readonly upgradeAssistant: string; + readonly upgradeAssistant: { + readonly overview: string; + readonly batchReindex: string; + readonly remoteReindex: string; + }; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts index 33085bdbf4478..1241d6222a38f 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -103,8 +103,13 @@ export const useRequest = ( : serializedResponseData; setData(responseData); } - // Setting isLoading to false also acts as a signal for scheduling the next poll request. - setIsLoading(false); + // There can be situations in which a component that consumes this hook gets unmounted when + // the request returns an error. So before changing the isLoading state, check if the component + // is still mounted. + if (isMounted.current === true) { + // Setting isLoading to false also acts as a signal for scheduling the next poll request. + setIsLoading(false); + } }, [requestBody, httpClient, deserializer, clearPollInterval] ); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 8ac619d479bef..e9008a1196700 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -8079,44 +8079,6 @@ } } } - }, - "ui_open": { - "properties": { - "elasticsearch": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the list of Elasticsearch deprecations." - } - }, - "overview": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the Overview page." - } - }, - "kibana": { - "type": "long", - "_meta": { - "description": "Number of times a user viewed the list of Kibana deprecations" - } - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "type": "long" - }, - "open": { - "type": "long" - }, - "start": { - "type": "long" - }, - "stop": { - "type": "long" - } - } } } }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 95bfc395e78f7..107ff1bc546dc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25100,28 +25100,14 @@ "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "ヘルプ", "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "無効な形式:{message}", "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "無効なフォーマット。例:{exampleUrl}", - "xpack.upgradeAssistant.appTitle": "{version} アップグレードアシスタント", "xpack.upgradeAssistant.breadcrumb.esDeprecationsLabel": "Elasticsearchの廃止予定", "xpack.upgradeAssistant.breadcrumb.kibanaDeprecationsLabel": "Kibanaの廃止予定", "xpack.upgradeAssistant.breadcrumb.overviewLabel": "アップグレードアシスタント", - "xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel": "より多く表示させるにはフィルターを変更します。", - "xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel": "重大", - "xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel": "インデックス別", - "xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel": "問題別", - "xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip": "アップグレード前にこの問題を解決してください。", - "xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel": "重大", - "xpack.upgradeAssistant.checkupTab.deprecations.warningActionTooltip": "アップグレード前にこの問題を解決することをお勧めしますが、必須ではありません。", - "xpack.upgradeAssistant.checkupTab.deprecations.warningLabel": "警告", - "xpack.upgradeAssistant.checkupTab.noDeprecationsLabel": "説明がありません", - "xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel": "{total} 件中 {numShown} 件を表示中", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel": "キャンセル", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel": "閉じる", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.continueButtonLabel": "再インデックスを続ける", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.insufficientPrivilegeCallout.calloutTitle": "このインデックスを再インデックスするための権限がありません", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail": "再インデックスはバックグラウンドで継続しますが、Kibana がシャットダウンまたは再起動した場合、このページに戻り再インデックスを再開させる必要があります。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.calloutTitle": "インデックスは再インデックス中にドキュメントを投入、更新、または削除できません", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.cantStopDetail": "ドキュメントの更新を停止できない場合、または新しいクラスターに再インデックスする必要がある場合は、異なるアップグレード方法をお勧めします。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.doneLabel": "完了!", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.reindexingLabel": "再インデックス中…", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.resumeLabel": "再開", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel": "再インデックスを実行", @@ -25132,17 +25118,9 @@ "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel": "キャンセル中…", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel": "キャンセルできませんでした", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle": "新規インデックスを作成中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.pauseMlStepTitle": "機械学習ジョブを一時停止中", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle": "古いインデックスを読み込み専用に設定中", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle": "ドキュメントを再インデックス中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeMlStepTitle": "機械学習ジョブを再開中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeWatcherStepTitle": "Watcher を再開中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.stopWatcherStepTitle": "Watcher を停止中", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklistTitle": "プロセスを再インデックス中", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails": "このインデックスは現在閉じています。アップグレードアシスタントが開き、再インデックスを実行してからインデックスを閉じます。 {reindexingMayTakeLongerEmph}。詳細については {docs} をご覧ください。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails.reindexingTakesLongerEmphasis": "再インデックスには通常よりも時間がかかることがあります", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutTitle": "インデックスが閉じました", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.openAndCloseDocumentation": "ドキュメンテーション", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningDetail": "マッピングタイプは8.0ではサポートされていません。アプリケーションコードまたはスクリプトが{mappingType}に依存していないことを確認してください。", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningTitle": "マッピングタイプ{mappingType}を{defaultType}で置き換えます", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.deprecatedIndexSettingsWarningDetail": "次の廃止予定のインデックス設定が検出されました。", @@ -25150,16 +25128,6 @@ "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail": "続行する前に、インデックスをバックアップしてください。再インデックスを続行するには、各変更を承諾してください。", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle": "このインデックスには元に戻すことのできない破壊的な変更が含まれています", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.documentationLinkLabel": "ドキュメント", - "xpack.upgradeAssistant.deprecationGroupItem.docLinkText": "ドキュメンテーションを表示", - "xpack.upgradeAssistant.deprecationGroupItem.fixButtonLabel": "修正する手順を表示", - "xpack.upgradeAssistant.deprecationGroupItem.resolveButtonLabel": "クイック解決", - "xpack.upgradeAssistant.deprecationGroupItemTitle": "'{domainId}'は廃止予定の機能を使用しています", - "xpack.upgradeAssistant.deprecationListBar.collapseAllButtonLabel": "すべて縮小", - "xpack.upgradeAssistant.deprecationListBar.expandAllButtonLabel": "すべて拡張", - "xpack.upgradeAssistant.deprecationListSearchBar.filterErrorMessageLabel": "フィルター無効:{searchTermError}", - "xpack.upgradeAssistant.deprecationListSearchBar.placeholderAriaLabel": "フィルター", - "xpack.upgradeAssistant.deprecationListSearchBar.placeholderLabel": "フィルター", - "xpack.upgradeAssistant.deprecationListSearchBar.reloadButtonLabel": "再読み込み", "xpack.upgradeAssistant.emptyPrompt.learnMoreDescription": "{nextMajor}への移行に関する詳細をご覧ください。", "xpack.upgradeAssistant.emptyPrompt.title": "{uaVersion} アップグレードアシスタント", "xpack.upgradeAssistant.emptyPrompt.upgradeAssistantDescription": "アップグレードアシスタントはクラスターの廃止予定の設定を特定し、アップグレード前に問題を解決できるようにします。Elastic {nextMajor}にアップグレードするときにここに戻って確認してください。", @@ -25178,35 +25146,10 @@ "xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.upgradeSnapshotErrorTitle": "スナップショットのアップグレードエラー", "xpack.upgradeAssistant.esDeprecations.pageDescription": "廃止予定のクラスターとインデックス設定をレビューします。アップグレード前に重要な問題を解決する必要があります。", "xpack.upgradeAssistant.esDeprecations.pageTitle": "Elasticsearch", - "xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsLabel": "このクラスター{criticalDeprecations}には重大な廃止予定があります", - "xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsTitle": "重大", - "xpack.upgradeAssistant.esDeprecationStats.loadingText": "Elasticsearchの廃止統計情報を読み込んでいます...", - "xpack.upgradeAssistant.esDeprecationStats.noDeprecationsText": "警告なし。準備ができました。", - "xpack.upgradeAssistant.esDeprecationStats.statsTitle": "Elasticsearch", - "xpack.upgradeAssistant.esDeprecationStats.warningDeprecationsTitle": "警告", - "xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorDescription": "エラーについては、Kibanaサーバーログを確認してください。", - "xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorTitle": "Kibana廃止予定を取得できませんでした", - "xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorDescription": "エラーについては、Kibanaサーバーログを確認してください。", - "xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorTitle": "一部のKibana廃止予定が正常に取得されませんでした", "xpack.upgradeAssistant.kibanaDeprecations.deprecationLabel": "Kibana", "xpack.upgradeAssistant.kibanaDeprecations.docLinkText": "ドキュメント", - "xpack.upgradeAssistant.kibanaDeprecations.errorMessage": "廃止予定の解決エラー", "xpack.upgradeAssistant.kibanaDeprecations.loadingText": "廃止予定を読み込んでいます...", - "xpack.upgradeAssistant.kibanaDeprecations.pageDescription": "アップグレード前に、ここで一覧の問題を確認し、必要な変更を行ってください。アップグレード前に、重大な問題を解決する必要があります。", "xpack.upgradeAssistant.kibanaDeprecations.pageTitle": "Kibana", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.cancelButtonLabel": "キャンセル", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.modalTitle": "'{domainId}'で廃止予定を解決しますか?", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.resolveButtonLabel": "解決", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.closeButtonLabel": "閉じる", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.docLinkLabel": "ドキュメンテーションを表示", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalTitle": "'{domainId}'で廃止予定を解決", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.stepTitle": "ステップ{step}", - "xpack.upgradeAssistant.kibanaDeprecations.successMessage": "廃止予定が解決されました", - "xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsTitle": "重大", - "xpack.upgradeAssistant.kibanaDeprecationStats.loadingErrorMessage": "Kibana廃止予定の取得中にエラーが発生しました。", - "xpack.upgradeAssistant.kibanaDeprecationStats.loadingText": "Kibana廃止予定統計情報を読み込んでいます…", - "xpack.upgradeAssistant.kibanaDeprecationStats.statsTitle": "Kibana", - "xpack.upgradeAssistant.kibanaDeprecationStats.warningDeprecationsTitle": "警告", "xpack.upgradeAssistant.noDeprecationsPrompt.nextStepsDescription": "他のスタック廃止予定については、{overviewButton}を確認してください。", "xpack.upgradeAssistant.noDeprecationsPrompt.overviewLinkText": "概要ページ", "xpack.upgradeAssistant.overview.analyzeTitle": "廃止予定ログを分析", @@ -25226,8 +25169,6 @@ "xpack.upgradeAssistant.overview.observe.observabilityDescription": "使用中のAPIのうち廃止予定のAPIと、更新が必要なアプリケーションを特定できます。", "xpack.upgradeAssistant.overview.pageDescription": "次のバージョンのElastic Stackをお待ちください。", "xpack.upgradeAssistant.overview.pageTitle": "アップグレードアシスタント", - "xpack.upgradeAssistant.overview.reviewStepTitle": "廃止予定設定を確認し、問題を解決", - "xpack.upgradeAssistant.overview.toggleTitle": "Elasticsearch廃止予定警告をログに出力", "xpack.upgradeAssistant.overview.upgradeGuideLink": "アップグレードガイドを表示", "xpack.upgradeAssistant.overview.upgradeStepCloudLink": "クラウドでアップグレード", "xpack.upgradeAssistant.overview.upgradeStepDescription": "重要な問題をすべて解決し、アプリケーションの準備を確認した後に、Elastic Stackをアップグレードできます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 921bd74939afa..31f44408917c5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25526,28 +25526,14 @@ "xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText": "帮助", "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage": "格式无效:{message}", "xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "格式无效。例如:{exampleUrl}", - "xpack.upgradeAssistant.appTitle": "{version} 升级助手", "xpack.upgradeAssistant.breadcrumb.esDeprecationsLabel": "Elasticsearch 弃用", "xpack.upgradeAssistant.breadcrumb.kibanaDeprecationsLabel": "Kibana 弃用", "xpack.upgradeAssistant.breadcrumb.overviewLabel": "升级助手", - "xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel": "更改筛选以显示更多内容。", - "xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel": "紧急", - "xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel": "按索引", - "xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel": "按问题", - "xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip": "请解决此问题后再升级。", - "xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel": "紧急", - "xpack.upgradeAssistant.checkupTab.deprecations.warningActionTooltip": "建议在升级之前先解决此问题,但这不是必需的。", - "xpack.upgradeAssistant.checkupTab.deprecations.warningLabel": "警告", - "xpack.upgradeAssistant.checkupTab.noDeprecationsLabel": "无弃用内容", - "xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel": "显示 {numShown} 个,共 {total} 个", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel": "取消", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel": "关闭", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.continueButtonLabel": "继续重新索引", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.insufficientPrivilegeCallout.calloutTitle": "您没有足够的权限来重新索引此索引", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.backgroundResumeDetail": "重新索引将在后台继续,但如果 Kibana 关闭或重新启动,您将需要返回此页,才能恢复重新索引。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.calloutTitle": "在重新索引时,索引无法采集、更新或删除文档", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.readonlyCallout.cantStopDetail": "如果您无法停止文档更新或需要重新索引到新的集群中,请考虑使用不同的升级策略。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.doneLabel": "已完成!", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.reindexingLabel": "正在重新索引……", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.resumeLabel": "恢复", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexButton.runReindexLabel": "运行重新索引", @@ -25558,17 +25544,9 @@ "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.cancellingLabel": "正在取消……", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.cancelButton.errorLabel": "无法取消", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.createIndexStepTitle": "正在创建新索引", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.pauseMlStepTitle": "正在暂停 Machine Learning 作业", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.readonlyStepTitle": "正在将旧索引设置为只读", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.reindexingDocumentsStepTitle": "正在重新索引文档", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeMlStepTitle": "正在恢复 Machine Learning 作业", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.resumeWatcherStepTitle": "正在恢复 Watcher", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklist.stopWatcherStepTitle": "正在停止 Watcher", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.reindexingChecklistTitle": "重新索引过程", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails": "此索引当前已关闭。升级助手将打开索引,重新索引,然后关闭索引。{reindexingMayTakeLongerEmph}。请参阅文档{docs}以了解更多信息。", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails.reindexingTakesLongerEmphasis": "重新索引可能比通常花费更多的时间", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutTitle": "索引已关闭", - "xpack.upgradeAssistant.checkupTab.reindexing.flyout.openAndCloseDocumentation": "文档", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningDetail": "映射类型在 8.0 中不再受支持。确保没有应用程序代码或脚本依赖 {mappingType}。", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.customTypeNameWarningTitle": "将映射类型 {mappingType} 替换为 {defaultType}", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.deprecatedIndexSettingsWarningDetail": "检测到以下弃用的索引设置:", @@ -25576,16 +25554,6 @@ "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutDetail": "继续前备份索引。要继续重新索引,请接受每个更改。", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.destructiveCallout.calloutTitle": "此索引需要无法恢复的破坏性更改", "xpack.upgradeAssistant.checkupTab.reindexing.flyout.warningsStep.documentationLinkLabel": "文档", - "xpack.upgradeAssistant.deprecationGroupItem.docLinkText": "查看文档", - "xpack.upgradeAssistant.deprecationGroupItem.fixButtonLabel": "显示修复步骤", - "xpack.upgradeAssistant.deprecationGroupItem.resolveButtonLabel": "快速解决", - "xpack.upgradeAssistant.deprecationGroupItemTitle": "“{domainId}”正在使用弃用的功能", - "xpack.upgradeAssistant.deprecationListBar.collapseAllButtonLabel": "折叠全部", - "xpack.upgradeAssistant.deprecationListBar.expandAllButtonLabel": "展开全部", - "xpack.upgradeAssistant.deprecationListSearchBar.filterErrorMessageLabel": "筛选无效:{searchTermError}", - "xpack.upgradeAssistant.deprecationListSearchBar.placeholderAriaLabel": "筛选", - "xpack.upgradeAssistant.deprecationListSearchBar.placeholderLabel": "筛选", - "xpack.upgradeAssistant.deprecationListSearchBar.reloadButtonLabel": "重新加载", "xpack.upgradeAssistant.emptyPrompt.learnMoreDescription": "详细了解如何迁移到 {nextMajor}。", "xpack.upgradeAssistant.emptyPrompt.title": "{uaVersion} 升级助手", "xpack.upgradeAssistant.emptyPrompt.upgradeAssistantDescription": "升级助手识别集群中弃用的设置,帮助您在升级前解决问题。需要升级到 Elastic {nextMajor} 时,回到这里查看。", @@ -25604,37 +25572,10 @@ "xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.upgradeSnapshotErrorTitle": "升级快照时出错", "xpack.upgradeAssistant.esDeprecations.pageDescription": "查看已弃用的群集和索引设置。在升级之前必须解决任何紧急问题。", "xpack.upgradeAssistant.esDeprecations.pageTitle": "Elasticsearch", - "xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsLabel": "此集群具有 {criticalDeprecations} 个关键弃用", - "xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsTitle": "紧急", - "xpack.upgradeAssistant.esDeprecationStats.loadingText": "正在加载 Elasticsearch 弃用统计……", - "xpack.upgradeAssistant.esDeprecationStats.noDeprecationsText": "无警告。已就绪!", - "xpack.upgradeAssistant.esDeprecationStats.statsTitle": "Elasticsearch", - "xpack.upgradeAssistant.esDeprecationStats.warningDeprecationsTitle": "警告", - "xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorDescription": "请在 Kibana 服务器日志中查看错误。", - "xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorTitle": "无法检索 Kibana 弃用", - "xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorDescription": "请在 Kibana 服务器日志中查看错误。", - "xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorTitle": "未成功检索全部的 Kibana 弃用", "xpack.upgradeAssistant.kibanaDeprecations.deprecationLabel": "Kibana", "xpack.upgradeAssistant.kibanaDeprecations.docLinkText": "文档", - "xpack.upgradeAssistant.kibanaDeprecations.errorMessage": "解决弃用时出错", "xpack.upgradeAssistant.kibanaDeprecations.loadingText": "正在加载弃用……", - "xpack.upgradeAssistant.kibanaDeprecations.pageDescription": "在升级之前查看此处所列的问题并进行必要的更改。在升级之前必须解决紧急问题。", "xpack.upgradeAssistant.kibanaDeprecations.pageTitle": "Kibana", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.cancelButtonLabel": "取消", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.modalTitle": "在“{domainId}”中解决弃用?", - "xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.resolveButtonLabel": "解决", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.closeButtonLabel": "关闭", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.docLinkLabel": "查看文档", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalTitle": "在“{domainId}”中解决弃用", - "xpack.upgradeAssistant.kibanaDeprecations.stepsModal.stepTitle": "步骤 {step}", - "xpack.upgradeAssistant.kibanaDeprecations.successMessage": "弃用已解决", - "xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsLabel": "Kibana 有 {criticalDeprecations} 个紧急{criticalDeprecations, plural, other {弃用}}", - "xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsTitle": "紧急", - "xpack.upgradeAssistant.kibanaDeprecationStats.getWarningDeprecationsMessage": "Kibana 有 {warningDeprecations} 个警告{warningDeprecations, plural, other {弃用}}", - "xpack.upgradeAssistant.kibanaDeprecationStats.loadingErrorMessage": "检索 Kibana 弃用时发生错误。", - "xpack.upgradeAssistant.kibanaDeprecationStats.loadingText": "正在加载 Kibana 弃用统计……", - "xpack.upgradeAssistant.kibanaDeprecationStats.statsTitle": "Kibana", - "xpack.upgradeAssistant.kibanaDeprecationStats.warningDeprecationsTitle": "警告", "xpack.upgradeAssistant.noDeprecationsPrompt.nextStepsDescription": "查看{overviewButton}以了解其他 Stack 弃用。", "xpack.upgradeAssistant.noDeprecationsPrompt.overviewLinkText": "“概览”页面", "xpack.upgradeAssistant.overview.analyzeTitle": "分析弃用日志", @@ -25654,8 +25595,6 @@ "xpack.upgradeAssistant.overview.observe.observabilityDescription": "深入了解正在使用哪些已弃用 API 以及需要更新哪些应用程序。", "xpack.upgradeAssistant.overview.pageDescription": "准备使用下一版 Elastic Stack!", "xpack.upgradeAssistant.overview.pageTitle": "升级助手", - "xpack.upgradeAssistant.overview.reviewStepTitle": "复查已弃用设置并解决问题", - "xpack.upgradeAssistant.overview.toggleTitle": "记录 Elasticsearch 弃用警告", "xpack.upgradeAssistant.overview.upgradeGuideLink": "查看升级指南", "xpack.upgradeAssistant.overview.upgradeStepCloudLink": "在 Cloud 上升级", "xpack.upgradeAssistant.overview.upgradeStepDescription": "解决所有关键问题并确认您的应用程序就绪后,便可以升级 Elastic Stack。", diff --git a/x-pack/plugins/upgrade_assistant/README.md b/x-pack/plugins/upgrade_assistant/README.md index a6cb3b431c82b..6570e7f8d7617 100644 --- a/x-pack/plugins/upgrade_assistant/README.md +++ b/x-pack/plugins/upgrade_assistant/README.md @@ -2,66 +2,253 @@ ## About -Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary -purposes are to: +Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. It will only be enabled on the last minor before the next major release. This is controlled via the config: `xpack.upgrade_assistant.readonly` ([#101296](https://github.com/elastic/kibana/pull/101296)). -* **Surface deprecations.** Deprecations are features that are currently being used that will be -removed in the next major. Surfacing tells the user that there's a problem preventing them -from upgrading. -* **Migrate from deprecation features to supported features.** This addresses the problem, clearing -the path for the upgrade. Generally speaking, once all deprecations are addressed, the user can -safely upgrade. +Its primary purposes are to: + +* **Surface deprecations.** Deprecations are features that are currently being used that will be removed in the next major. Surfacing tells the user that there's a problem preventing them from upgrading. +* **Migrate from deprecated features to supported features.** This addresses the problem, clearing the path for the upgrade. Generally speaking, once all deprecations are addressed, the user can safely upgrade. ### Deprecations -There are two sources of deprecation information: +There are three sources of deprecation information: -* [**Deprecation Info API.**](https://www.elastic.co/guide/en/elasticsearch/reference/master/migration-api-deprecation.html) -This is information about cluster, node, and index level settings that use deprecated features that -will be removed or changed in the next major version. Currently, only cluster and index deprecations -will be surfaced in the Upgrade Assistant. ES server engineers are responsible for adding -deprecations to the Deprecation Info API. -* [**Deprecation logs.**](https://www.elastic.co/guide/en/elasticsearch/reference/current/logging.html#deprecation-logging) +* [**Elasticsearch Deprecation Info API.**](https://www.elastic.co/guide/en/elasticsearch/reference/master/migration-api-deprecation.html) +This is information about Elasticsearch cluster, node, Machine Learning, and index-level settings that use deprecated features that will be removed or changed in the next major version. ES server engineers are responsible for adding deprecations to the Deprecation Info API. +* [**Elasticsearch deprecation logs.**](https://www.elastic.co/guide/en/elasticsearch/reference/current/logging.html#deprecation-logging) These surface runtime deprecations, e.g. a Painless script that uses a deprecated accessor or a request to a deprecated API. These are also generally surfaced as deprecation headers within the response. Even if the cluster state is good, app maintainers need to watch the logs in case -deprecations are discovered as data is migrated. +deprecations are discovered as data is migrated. Starting in 7.x, deprecation logs can be written to a file or a data stream ([#58924](https://github.com/elastic/elasticsearch/pull/58924)). When the data stream exists, the Upgrade Assistant provides a way to analyze the logs through Observability or Discover ([#106521](https://github.com/elastic/kibana/pull/106521)). +* [**Kibana deprecations API.**](https://github.com/elastic/kibana/blob/master/src/core/server/deprecations/README.mdx) This is information about deprecated features and configs in Kibana. These deprecations are only communicated to the user if the deployment is using these features. Kibana engineers are responsible for adding deprecations to the deprecations API for their respective team. ### Fixing problems -Problems can be fixed at various points in the upgrade process. The Upgrade Assistant supports -various upgrade paths and surfaces various types of upgrade-related issues. - -* **Fixing deprecated cluster settings pre-upgrade.** This generally requires fixing some settings -in `elasticsearch.yml`. -* **Migrating indices data pre-upgrade.** This can involve deleting indices so that ES can rebuild -them in the new version, reindexing them so that they're built using a new Lucene version, or -applying a migration script that reindexes them with new settings/mappings/etc. -* **Migrating indices data post-upgrade.** As was the case with APM in the 6.8->7.x upgrade, -sometimes the new data format isn't forwards-compatible. In these cases, the user will perform the -upgrade first and then use the Upgrade Assistant to reindex their data to be compatible with the new -version. - -Deprecations can be handled in a number of ways: - -* **Reindexing.** When a user's index contains deprecations (e.g. mappings) a reindex solves them. -Upgrade Assistant contains migration scripts that are executed as part of the reindex process. -The user will see a "Reindex" button they can click which will apply this script and perform the -reindex. +#### Elasticsearch + +Elasticsearch deprecations can be handled in a number of ways: + +* **Reindexing.** When a user's index contains deprecations (e.g. mappings) a reindex solves them. Currently, the Upgrade Assistant only automates reindexing for old indices. For example, if you are currently on 7.x, and want to migrate to 8.0, but you still have indices that were created on 6.x. For this scenario, the user will see a "Reindex" button that they can click, which will perform the reindex. * Reindexing is an atomic process in Upgrade Assistant, so that ingestion is never disrupted. It works like this: * Create a new index with a "reindexed-" prefix ([#30114](https://github.com/elastic/kibana/pull/30114)). * Create an index alias pointing from the original index name to the prefixed index name. * Reindex from the original index into the prefixed index. * Delete the old index and rename the prefixed index. - * Some apps might require custom scripts, as was the case with APM ([#29845](https://github.com/elastic/kibana/pull/29845)). - In that case the migration performed a reindex with a Painless script (covered by automated tests) - that made the required changes to the data. -* **Update index settings.** Some index settings will need to be updated, which doesn't require a -reindex. An example of this is the "Fix" button that was added for metricbeat and filebeat indices -([#32829](https://github.com/elastic/kibana/pull/32829), [#33439](https://github.com/elastic/kibana/pull/33439)). +* **Updating index settings.** Some index settings will need to be updated, which doesn't require a +reindex. An example of this is the "Remove deprecated settings" button, which is shown when [deprecated translog settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html#index-modules-translog-retention) are detected ([#93293](https://github.com/elastic/kibana/pull/93293)). +* **Upgrading or deleting snapshots**. This is specific to Machine Learning. If a user has old Machine Learning job model snapshots, they will need to be upgraded or deleted. The Upgrade Assistant provides a way to resolve this automatically for the user ([#100066](https://github.com/elastic/kibana/pull/100066)). * **Following the docs.** The Deprecation Info API provides links to the deprecation docs. Users will follow these docs to address the problem and make these warnings or errors disappear in the Upgrade Assistant. -* **Stopping/restarting tasks and jobs.** Users had to stop watches and ML jobs and restart them as -soon as reindexing was complete ([#29663](https://github.com/elastic/kibana/pull/29663)). \ No newline at end of file + +#### Kibana + +Kibana deprecations can be handled in one of two ways: + +* **Automatic resolution.** Some deprecations can be fixed automatically through Upgrade Assistant via an API call. When this is possible, users will see a "Quick resolve" button in the Upgrade Assistant. +* **Manual steps.** For deprecations that require the user to address manually, the Upgrade Assistant provides a list of steps to follow as well as a link to documentation. Once the deprecation is addressed, it will no longer appear in the Upgrade Assistant. + +### Steps for testing +#### Elasticsearch deprecations + +To test the Elasticsearch deprecations page ([#107053](https://github.com/elastic/kibana/pull/107053)), you will first need to create a set of deprecations that will be returned from the deprecation info API. + +**1. Reindexing** + + The reindex action appears in UA whenever the deprecation `Index created before XX` is encountered. To reproduce, you will need to start up a cluster on the previous major version (e.g., if you are running 7.x, start a 6.8 cluster). Create a handful of indices, for example: + + ``` + PUT my_index + ``` + + Next, point to the 6.x data directory when running from a 7.x cluster. + + ``` + yarn es snapshot -E path.data=./path_to_6.x_indices + ``` + + **Token-based authentication** + + Reindexing should also work using token-based authentication (implemented via [#111451](https://github.com/elastic/kibana/pull/111451)). To simulate, set the following parameters when running ES from a snapshot: + + ``` + yarn es snapshot -E path.data=./path_to_6.x_indices -E xpack.security.authc.token.enabled=true -E xpack.security.authc.api_key.enabled=true + ``` + + Then, update your `kibana.dev.yml` file to include: + + ``` + xpack.security.authc.providers: + token: + token1: + order: 0 + showInSelector: true + enabled: true + ``` + + To verify it's working as expected, kick off a reindex task in UA. Then, navigate to **Security > API keys** and verify an API key was created. The name should be prefixed with `ua_reindex_`. Once the reindex task has completed successfully, the API key should be deleted. + +**2. Upgrading or deleting ML job model snapshots** + + Similar to the reindex action, the ML action requires setting up a cluster on the previous major version. It also requires the trial license to be enabled. Then, you will need to create a few ML jobs in order to trigger snapshots. + + - Add the Kibana sample data. + - Navigate to Machine Learning > Create new job. + - Select `kibana_sample_data_flights` index. + - Select "Single metric job". + - Add an aggregation, field, and job ID. Change the time range to "Absolute" and select a subset of time. + - Click "Create job" + - View the job created and click the "Start datafeed" action associated with it. Select a subset of time and click "Start". You should now have two snapshots created. If you want to add more, repeat the steps above. + + Next, point to the 6.x data directory when running from a 7.x cluster. + + ``` + yarn es snapshot --license trial -E path.data=./path_to_6.x_ml_snapshots + ``` + +**3. Removing deprecated index settings** + + The Upgrade Assistant currently only supports fixing deprecated translog index settings. However [the code](https://github.com/elastic/kibana/blob/master/x-pack/plugins/upgrade_assistant/common/constants.ts#L22) is written in a way to add support for more if necessary. Run the following Console command to trigger the deprecation warning: + + ``` + PUT deprecated_settings + { + "settings": { + "translog.retention.size": "1b", + "translog.retention.age": "5m", + "index.soft_deletes.enabled": true, + } + } + ``` + +**4. Other deprecations with no automatic resolutions** + + Many deprecations emitted from the deprecation info API are too complex to provide an automatic resolution for in UA. In this case, UA provides details about the deprecation as well as a link to documentation. The following requests will emit deprecations from the deprecation info API. This list is *not* exhaustive of all possible deprecations. You can find the full list of [7.x deprecations in the Elasticsearch repo](https://github.com/elastic/elasticsearch/tree/7.x/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation) by grepping `new DeprecationIssue` in the code. + + ``` + PUT /nested_multi_fields + { + "mappings":{ + "properties":{ + "text":{ + "type":"text", + "fields":{ + "english":{ + "type":"text", + "analyzer":"english", + "fields":{ + "english":{ + "type":"text", + "analyzer":"english" + } + } + } + } + } + } + } + } + ``` + + ``` + PUT field_names_enabled + { + "mappings": { + "_field_names": { + "enabled": false + } + } + } + ``` + + ``` + PUT /_cluster/settings + { + "persistent" : { + "indices.lifecycle.poll_interval" : "500ms" + } + } + ``` + + ``` + PUT _template/field_names_enabled + { + "index_patterns": ["foo"], + "mappings": { + "_field_names": { + "enabled": false + } + } + } + ``` + + ``` + // This is only applicable for indices created prior to 7.x + PUT joda_time + { + "mappings" : { + "properties" : { + "datetime": { + "type": "date", + "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis" + } + } + } + } + ``` + +#### Kibana deprecations +To test the Kibana deprecations page, you will first need to create a set of deprecations that will be returned from the Kibana deprecations API. + +`reporting` is currently one of the only plugins that is registering a deprecation with an automated resolution (implemented via [#104303](https://github.com/elastic/kibana/pull/104303)). To trigger this deprecation: + +1. Add Kibana sample data. +2. Create a PDF report from the Dashboard (**Dashboard > Share > PDF reports > Generate PDFs**). This requires a trial license. +3. Issue the following request in Console: + +``` +PUT .reporting-*/_settings +{ + "settings": { + "index.lifecycle.name": null + } +} +``` + +For a complete list of Kibana deprecations, refer to the [8.0 Kibana deprecations meta issue](https://github.com/elastic/kibana/issues/109166). + +### Errors + +This is a non-exhaustive list of different error scenarios in Upgrade Assistant. It's recommended to use the [tweak browser extension](https://chrome.google.com/webstore/detail/tweak-mock-api-calls/feahianecghpnipmhphmfgmpdodhcapi?hl=en), or something similar, to mock the API calls. + +- **Error loading deprecation logging status.** Mock a `404` status code to `GET /api/upgrade_assistant/deprecation_logging`. Alternatively, edit [this line](https://github.com/elastic/kibana/blob/545c1420c285af8f5eee56f414bd6eca735aea11/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts#L70) locally and replace `deprecation_logging` with `fake_deprecation_logging`. +- **Error updating deprecation logging status.** Mock a `404` status code to `PUT /api/upgrade_assistant/deprecation_logging`. Alternatively, edit [this line](https://github.com/elastic/kibana/blob/545c1420c285af8f5eee56f414bd6eca735aea11/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts#L77) locally and replace `deprecation_logging` with `fake_deprecation_logging`. +- **Unauthorized error fetching ES deprecations.** Mock a `403` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 403 }` +- **Partially upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": false } }` +- **Upgraded error fetching ES deprecations.** Mock a `426` status code to `GET /api/upgrade_assistant/es_deprecations` with the response payload: `{ "statusCode": 426, "attributes": { "allNodesUpgraded": true } }` + +### Telemetry + +The Upgrade Assistant tracks several triggered events in the UI, using Kibana Usage Collection service's [UI counters](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#ui-counters). + +**Overview page** +- Component loaded +- Click event for "Create snapshot" button +- Click event for "View deprecation logs in Observability" link +- Click event for "Analyze logs in Discover" link +- Click event for "Reset counter" button + +**ES deprecations page** +- Component loaded +- Click events for starting and stopping reindex tasks +- Click events for upgrading or deleting a Machine Learning snapshot +- Click event for deleting a deprecated index setting + +**Kibana deprecations page** +- Component loaded +- Click event for "Quick resolve" button + +In addition to UI counters, the Upgrade Assistant has a [custom usage collector](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#custom-collector). It currently is only responsible for tracking whether the user has deprecation logging enabled or not. + +For testing instructions, refer to the [Kibana Usage Collection service README](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#testing). \ No newline at end of file diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/app.helpers.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/app.helpers.tsx new file mode 100644 index 0000000000000..23726e05b895d --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/app.helpers.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 { act } from 'react-dom/test-utils'; +import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; + +import { App } from '../../../public/application/app'; +import { WithAppDependencies } from '../helpers'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`/overview`], + componentRoutePath: '/overview', + }, + doMountAsync: true, +}; + +export type AppTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const clickDeprecationToggle = async () => { + const { find, component } = testBed; + + await act(async () => { + find('deprecationLoggingToggle').simulate('click'); + }); + + component.update(); + }; + + return { + clickDeprecationToggle, + }; +}; + +export const setupAppPage = async (overrides?: Record): Promise => { + const initTestBed = registerTestBed(WithAppDependencies(App, overrides), testBedConfig); + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/cluster_upgrade.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/cluster_upgrade.test.tsx new file mode 100644 index 0000000000000..043c649b39bc2 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/app/cluster_upgrade.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from '../helpers'; +import { AppTestBed, setupAppPage } from './app.helpers'; + +describe('Cluster upgrade', () => { + let testBed: AppTestBed; + let server: ReturnType['server']; + let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers']; + + beforeEach(() => { + ({ server, httpRequestsMockHelpers } = setupEnvironment()); + }); + + afterEach(() => { + server.restore(); + }); + + describe('when user is still preparing for upgrade', () => { + beforeEach(async () => { + testBed = await setupAppPage(); + }); + + test('renders overview', () => { + const { exists } = testBed; + expect(exists('overview')).toBe(true); + expect(exists('isUpgradingMessage')).toBe(false); + expect(exists('isUpgradeCompleteMessage')).toBe(false); + }); + }); + + describe('when cluster is in the process of a rolling upgrade', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(undefined, { + statusCode: 426, + message: '', + attributes: { + allNodesUpgraded: false, + }, + }); + + await act(async () => { + testBed = await setupAppPage(); + }); + }); + + test('renders rolling upgrade message', async () => { + const { component, exists } = testBed; + component.update(); + expect(exists('overview')).toBe(false); + expect(exists('isUpgradingMessage')).toBe(true); + expect(exists('isUpgradeCompleteMessage')).toBe(false); + }); + }); + + describe('when cluster has been upgraded', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(undefined, { + statusCode: 426, + message: '', + attributes: { + allNodesUpgraded: true, + }, + }); + + await act(async () => { + testBed = await setupAppPage(); + }); + }); + + test('renders upgrade complete message', () => { + const { component, exists } = testBed; + component.update(); + expect(exists('overview')).toBe(false); + expect(exists('isUpgradingMessage')).toBe(false); + expect(exists('isUpgradeCompleteMessage')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts index 917fac8ef666a..fdd8a1c993937 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts @@ -7,8 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; - +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './mocked_responses'; describe('Default deprecation flyout', () => { @@ -35,16 +35,19 @@ describe('Default deprecation flyout', () => { testBed.component.update(); }); - it('renders a flyout with deprecation details', async () => { + test('renders a flyout with deprecation details', async () => { const multiFieldsDeprecation = esDeprecationsMockResponse.deprecations[2]; const { actions, find, exists } = testBed; - await actions.clickDefaultDeprecationAt(0); + await actions.table.clickDeprecationRowAt('default', 0); expect(exists('defaultDeprecationDetails')).toBe(true); expect(find('defaultDeprecationDetails.flyoutTitle').text()).toContain( multiFieldsDeprecation.message ); + expect(find('defaultDeprecationDetails.documentationLink').props().href).toBe( + multiFieldsDeprecation.url + ); expect(find('defaultDeprecationDetails.flyoutDescription').text()).toContain( multiFieldsDeprecation.index ); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts index ceebc528f0bc4..3b8a756b8e64c 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts @@ -9,7 +9,8 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../common/constants'; import type { MlAction } from '../../../common/types'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, @@ -17,7 +18,7 @@ import { createEsDeprecationsMockResponse, } from './mocked_responses'; -describe('Deprecations table', () => { +describe('ES deprecations table', () => { let testBed: ElasticsearchTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -56,31 +57,49 @@ describe('Deprecations table', () => { const { actions } = testBed; const totalRequests = server.requests.length; - await actions.clickRefreshButton(); + await actions.table.clickRefreshButton(); const mlDeprecation = esDeprecationsMockResponse.deprecations[0]; const reindexDeprecation = esDeprecationsMockResponse.deprecations[3]; - // Since upgradeStatusMockResponse includes ML and reindex actions (which require fetching status), there will be 3 requests made - expect(server.requests.length).toBe(totalRequests + 3); - expect(server.requests[server.requests.length - 3].url).toBe( + // Since upgradeStatusMockResponse includes ML and reindex actions (which require fetching status), there will be 4 requests made + expect(server.requests.length).toBe(totalRequests + 4); + expect(server.requests[server.requests.length - 4].url).toBe( `${API_BASE_PATH}/es_deprecations` ); - expect(server.requests[server.requests.length - 2].url).toBe( + expect(server.requests[server.requests.length - 3].url).toBe( `${API_BASE_PATH}/ml_snapshots/${(mlDeprecation.correctiveAction as MlAction).jobId}/${ (mlDeprecation.correctiveAction as MlAction).snapshotId }` ); - expect(server.requests[server.requests.length - 1].url).toBe( + expect(server.requests[server.requests.length - 2].url).toBe( `${API_BASE_PATH}/reindex/${reindexDeprecation.index}` ); + + expect(server.requests[server.requests.length - 1].url).toBe( + `${API_BASE_PATH}/ml_upgrade_mode` + ); + }); + + it('shows critical and warning deprecations count', () => { + const { find } = testBed; + const criticalDeprecations = esDeprecationsMockResponse.deprecations.filter( + (deprecation) => deprecation.isCritical + ); + const warningDeprecations = esDeprecationsMockResponse.deprecations.filter( + (deprecation) => deprecation.isCritical === false + ); + + expect(find('criticalDeprecationsCount').text()).toContain(criticalDeprecations.length); + + expect(find('warningDeprecationsCount').text()).toContain(warningDeprecations.length); }); describe('search bar', () => { it('filters results by "critical" status', async () => { const { find, actions } = testBed; - await actions.clickCriticalFilterButton(); + await actions.searchBar.clickCriticalFilterButton(); const criticalDeprecations = esDeprecationsMockResponse.deprecations.filter( (deprecation) => deprecation.isCritical @@ -88,7 +107,7 @@ describe('Deprecations table', () => { expect(find('deprecationTableRow').length).toEqual(criticalDeprecations.length); - await actions.clickCriticalFilterButton(); + await actions.searchBar.clickCriticalFilterButton(); expect(find('deprecationTableRow').length).toEqual( esDeprecationsMockResponse.deprecations.length @@ -98,7 +117,7 @@ describe('Deprecations table', () => { it('filters results by type', async () => { const { component, find, actions } = testBed; - await actions.clickTypeFilterDropdownAt(0); + await actions.searchBar.clickTypeFilterDropdownAt(0); // We need to read the document "body" as the filter dropdown options are added there and not inside // the component DOM tree. @@ -125,7 +144,7 @@ describe('Deprecations table', () => { const { find, actions } = testBed; const multiFieldsDeprecation = esDeprecationsMockResponse.deprecations[2]; - await actions.setSearchInputValue(multiFieldsDeprecation.message); + await actions.searchBar.setSearchInputValue(multiFieldsDeprecation.message); expect(find('deprecationTableRow').length).toEqual(1); expect(find('deprecationTableRow').at(0).text()).toContain(multiFieldsDeprecation.message); @@ -134,7 +153,7 @@ describe('Deprecations table', () => { it('shows error for invalid search queries', async () => { const { find, exists, actions } = testBed; - await actions.setSearchInputValue('%'); + await actions.searchBar.setSearchInputValue('%'); expect(exists('invalidSearchQueryMessage')).toBe(true); expect(find('invalidSearchQueryMessage').text()).toContain('Invalid search'); @@ -143,7 +162,7 @@ describe('Deprecations table', () => { it('shows message when search query does not return results', async () => { const { find, actions, exists } = testBed; - await actions.setSearchInputValue('foobarbaz'); + await actions.searchBar.setSearchInputValue('foobarbaz'); expect(exists('noDeprecationsRow')).toBe(true); expect(find('noDeprecationsRow').text()).toContain( @@ -183,7 +202,7 @@ describe('Deprecations table', () => { expect(find('deprecationTableRow').length).toEqual(50); // Navigate to the next page - await actions.clickPaginationAt(1); + await actions.pagination.clickPaginationAt(1); // On the second (last) page, we expect to see the remaining deprecations expect(find('deprecationTableRow').length).toEqual(deprecations.length - 50); @@ -192,7 +211,7 @@ describe('Deprecations table', () => { it('allows the number of viewable rows to change', async () => { const { find, actions, component } = testBed; - await actions.clickRowsPerPageDropdown(); + await actions.pagination.clickRowsPerPageDropdown(); // We need to read the document "body" as the rows-per-page dropdown options are added there and not inside // the component DOM tree. @@ -219,7 +238,7 @@ describe('Deprecations table', () => { const criticalDeprecations = deprecations.filter((deprecation) => deprecation.isCritical); - await actions.clickCriticalFilterButton(); + await actions.searchBar.clickCriticalFilterButton(); // Only 40 critical deprecations, so only one page should show expect(find('esDeprecationsPagination').find('.euiPagination__item').length).toEqual(1); @@ -232,7 +251,7 @@ describe('Deprecations table', () => { (deprecation) => deprecation.correctiveAction?.type === 'reindex' ); - await actions.setSearchInputValue('Index created before 7.0'); + await actions.searchBar.setSearchInputValue('Index created before 7.0'); // Only 20 deprecations that match, so only one page should show expect(find('esDeprecationsPagination').find('.euiPagination__item').length).toEqual(1); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts index 8d3616a1b9d6b..2f0c8f0597ec3 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/error_handling.test.ts @@ -7,7 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; describe('Error handling', () => { let testBed: ElasticsearchTestBed; @@ -30,13 +31,10 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('permissionsError')).toBe(true); - expect(find('permissionsError').text()).toContain( - 'You are not authorized to view Elasticsearch deprecations.' + expect(find('deprecationsPageLoadingError').text()).toContain( + 'You are not authorized to view Elasticsearch deprecation issues.' ); }); @@ -58,12 +56,11 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('upgradedCallout')).toBe(true); - expect(find('upgradedCallout').text()).toContain('All Elasticsearch nodes have been upgraded.'); + expect(find('deprecationsPageLoadingError').text()).toContain( + 'All Elasticsearch nodes have been upgraded.' + ); }); it('shows partially upgrade error when nodes are running different versions', async () => { @@ -82,12 +79,9 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('partiallyUpgradedWarning')).toBe(true); - expect(find('partiallyUpgradedWarning').text()).toContain( + expect(find('deprecationsPageLoadingError').text()).toContain( 'Upgrade Kibana to the same version as your Elasticsearch cluster. One or more nodes in the cluster is running a different version than Kibana.' ); }); @@ -105,11 +99,10 @@ describe('Error handling', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { component, exists, find } = testBed; - + const { component, find } = testBed; component.update(); - - expect(exists('requestError')).toBe(true); - expect(find('requestError').text()).toContain('Could not retrieve Elasticsearch deprecations.'); + expect(find('deprecationsPageLoadingError').text()).toContain( + 'Could not retrieve Elasticsearch deprecation issues.' + ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/es_deprecations.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/es_deprecations.helpers.ts new file mode 100644 index 0000000000000..9bb44a9314c52 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/es_deprecations.helpers.ts @@ -0,0 +1,161 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; +import { EsDeprecations } from '../../../public/application/components/es_deprecations'; +import { WithAppDependencies } from '../helpers'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: ['/es_deprecations'], + componentRoutePath: '/es_deprecations', + }, + doMountAsync: true, +}; + +export type ElasticsearchTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const { component, find } = testBed; + + /** + * User Actions + */ + + const table = { + clickRefreshButton: async () => { + await act(async () => { + find('refreshButton').simulate('click'); + }); + + component.update(); + }, + clickDeprecationRowAt: async ( + deprecationType: 'mlSnapshot' | 'indexSetting' | 'reindex' | 'default', + index: number + ) => { + await act(async () => { + find(`deprecation-${deprecationType}`).at(index).simulate('click'); + }); + + component.update(); + }, + }; + + const searchBar = { + clickTypeFilterDropdownAt: async (index: number) => { + await act(async () => { + // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector + find('searchBarContainer') + .find('.euiPopover') + .find('.euiFilterButton') + .at(index) + .simulate('click'); + }); + + component.update(); + }, + setSearchInputValue: async (searchValue: string) => { + await act(async () => { + find('searchBarContainer') + .find('input') + .simulate('keyup', { target: { value: searchValue } }); + }); + + component.update(); + }, + clickCriticalFilterButton: async () => { + await act(async () => { + // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector + find('searchBarContainer').find('.euiFilterButton').at(0).simulate('click'); + }); + + component.update(); + }, + }; + + const pagination = { + clickPaginationAt: async (index: number) => { + await act(async () => { + find(`pagination-button-${index}`).simulate('click'); + }); + + component.update(); + }, + clickRowsPerPageDropdown: async () => { + await act(async () => { + find('tablePaginationPopoverButton').simulate('click'); + }); + + component.update(); + }, + }; + + const mlDeprecationFlyout = { + clickUpgradeSnapshot: async () => { + await act(async () => { + find('mlSnapshotDetails.upgradeSnapshotButton').simulate('click'); + }); + + component.update(); + }, + clickDeleteSnapshot: async () => { + await act(async () => { + find('mlSnapshotDetails.deleteSnapshotButton').simulate('click'); + }); + + component.update(); + }, + }; + + const indexSettingsDeprecationFlyout = { + clickDeleteSettingsButton: async () => { + await act(async () => { + find('deleteSettingsButton').simulate('click'); + }); + + component.update(); + }, + }; + + const reindexDeprecationFlyout = { + clickReindexButton: async () => { + await act(async () => { + find('startReindexingButton').simulate('click'); + }); + + component.update(); + }, + }; + + return { + table, + searchBar, + pagination, + mlDeprecationFlyout, + reindexDeprecationFlyout, + indexSettingsDeprecationFlyout, + }; +}; + +export const setupElasticsearchPage = async ( + overrides?: Record +): Promise => { + const initTestBed = registerTestBed( + WithAppDependencies(EsDeprecations, overrides), + testBedConfig + ); + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts index efeb78a507160..f62d24081ed56 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts @@ -7,8 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; - +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './mocked_responses'; describe('Index settings deprecation flyout', () => { @@ -33,27 +33,34 @@ describe('Index settings deprecation flyout', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { find, exists, actions, component } = testBed; - + const { actions, component } = testBed; component.update(); + await actions.table.clickDeprecationRowAt('indexSetting', 0); + }); - await actions.clickIndexSettingsDeprecationAt(0); + test('renders a flyout with deprecation details', async () => { + const { find, exists } = testBed; expect(exists('indexSettingsDetails')).toBe(true); expect(find('indexSettingsDetails.flyoutTitle').text()).toContain( indexSettingDeprecation.message ); + expect(find('indexSettingsDetails.documentationLink').props().href).toBe( + indexSettingDeprecation.url + ); expect(exists('removeSettingsPrompt')).toBe(true); }); it('removes deprecated index settings', async () => { - const { find, actions } = testBed; + const { find, actions, exists } = testBed; httpRequestsMockHelpers.setUpdateIndexSettingsResponse({ acknowledged: true, }); - await actions.clickDeleteSettingsButton(); + expect(exists('indexSettingsDetails.warningDeprecationBadge')).toBe(true); + + await actions.indexSettingsDeprecationFlyout.clickDeleteSettingsButton(); const request = server.requests[server.requests.length - 1]; @@ -69,12 +76,14 @@ describe('Index settings deprecation flyout', () => { ); // Reopen the flyout - await actions.clickIndexSettingsDeprecationAt(0); + await actions.table.clickDeprecationRowAt('indexSetting', 0); // Verify prompt to remove setting no longer displays expect(find('removeSettingsPrompt').length).toEqual(0); // Verify the action button no longer displays expect(find('indexSettingsDetails.deleteSettingsButton').length).toEqual(0); + // Verify the badge got marked as resolved + expect(exists('indexSettingsDetails.resolvedDeprecationBadge')).toBe(true); }); it('handles failure', async () => { @@ -87,7 +96,7 @@ describe('Index settings deprecation flyout', () => { httpRequestsMockHelpers.setUpdateIndexSettingsResponse(undefined, error); - await actions.clickDeleteSettingsButton(); + await actions.indexSettingsDeprecationFlyout.clickDeleteSettingsButton(); const request = server.requests[server.requests.length - 1]; @@ -103,7 +112,7 @@ describe('Index settings deprecation flyout', () => { ); // Reopen the flyout - await actions.clickIndexSettingsDeprecationAt(0); + await actions.table.clickDeprecationRowAt('indexSetting', 0); // Verify the flyout shows an error message expect(find('indexSettingsDetails.deleteSettingsError').text()).toContain( diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts index 909976355cd31..b24cd5a69a28e 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts @@ -8,7 +8,8 @@ import { act } from 'react-dom/test-utils'; import type { MlAction } from '../../../common/types'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './mocked_responses'; describe('Machine learning deprecation flyout', () => { @@ -22,6 +23,7 @@ describe('Machine learning deprecation flyout', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esDeprecationsMockResponse); + httpRequestsMockHelpers.setLoadMlUpgradeModeResponse({ mlUpgradeModeEnabled: false }); httpRequestsMockHelpers.setUpgradeMlSnapshotStatusResponse({ nodeId: 'my_node', snapshotId: MOCK_SNAPSHOT_ID, @@ -33,16 +35,19 @@ describe('Machine learning deprecation flyout', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { find, exists, actions, component } = testBed; - + const { actions, component } = testBed; component.update(); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); + }); - await actions.clickMlDeprecationAt(0); + test('renders a flyout with deprecation details', async () => { + const { find, exists } = testBed; expect(exists('mlSnapshotDetails')).toBe(true); expect(find('mlSnapshotDetails.flyoutTitle').text()).toContain( 'Upgrade or delete model snapshot' ); + expect(find('mlSnapshotDetails.documentationLink').props().href).toBe(mlDeprecation.url); }); describe('upgrade snapshots', () => { @@ -63,9 +68,10 @@ describe('Machine learning deprecation flyout', () => { status: 'complete', }); + expect(exists('mlSnapshotDetails.criticalDeprecationBadge')).toBe(true); expect(find('mlSnapshotDetails.upgradeSnapshotButton').text()).toEqual('Upgrade'); - await actions.clickUpgradeMlSnapshot(); + await actions.mlDeprecationFlyout.clickUpgradeSnapshot(); // First, we expect a POST request to upgrade the snapshot const upgradeRequest = server.requests[server.requests.length - 2]; @@ -83,11 +89,13 @@ describe('Machine learning deprecation flyout', () => { expect(find('mlActionResolutionCell').text()).toContain('Upgrade complete'); // Reopen the flyout - await actions.clickMlDeprecationAt(0); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); - // Flyout actions should not be visible if deprecation was resolved + // Flyout actions should be hidden if deprecation was resolved expect(exists('mlSnapshotDetails.upgradeSnapshotButton')).toBe(false); expect(exists('mlSnapshotDetails.deleteSnapshotButton')).toBe(false); + // Badge should be updated in flyout title + expect(exists('mlSnapshotDetails.resolvedDeprecationBadge')).toBe(true); }); it('handles upgrade failure', async () => { @@ -108,7 +116,7 @@ describe('Machine learning deprecation flyout', () => { error, }); - await actions.clickUpgradeMlSnapshot(); + await actions.mlDeprecationFlyout.clickUpgradeSnapshot(); const upgradeRequest = server.requests[server.requests.length - 1]; expect(upgradeRequest.method).toBe('POST'); @@ -118,7 +126,7 @@ describe('Machine learning deprecation flyout', () => { expect(find('mlActionResolutionCell').text()).toContain('Upgrade failed'); // Reopen the flyout - await actions.clickMlDeprecationAt(0); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); // Verify the flyout shows an error message expect(find('mlSnapshotDetails.resolveSnapshotError').text()).toContain( @@ -127,19 +135,41 @@ describe('Machine learning deprecation flyout', () => { // Verify the upgrade button text changes expect(find('mlSnapshotDetails.upgradeSnapshotButton').text()).toEqual('Retry upgrade'); }); + + it('Disables actions if ml_upgrade_mode is enabled', async () => { + httpRequestsMockHelpers.setLoadMlUpgradeModeResponse({ mlUpgradeModeEnabled: true }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + const { actions, exists, component } = testBed; + + component.update(); + + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); + + // Shows an error callout with a docs link + expect(exists('mlSnapshotDetails.mlUpgradeModeEnabledError')).toBe(true); + expect(exists('mlSnapshotDetails.setUpgradeModeDocsLink')).toBe(true); + // Flyout actions should be hidden + expect(exists('mlSnapshotDetails.upgradeSnapshotButton')).toBe(false); + expect(exists('mlSnapshotDetails.deleteSnapshotButton')).toBe(false); + }); }); describe('delete snapshots', () => { it('successfully deletes snapshots', async () => { - const { find, actions } = testBed; + const { find, actions, exists } = testBed; httpRequestsMockHelpers.setDeleteMlSnapshotResponse({ acknowledged: true, }); + expect(exists('mlSnapshotDetails.criticalDeprecationBadge')).toBe(true); expect(find('mlSnapshotDetails.deleteSnapshotButton').text()).toEqual('Delete'); - await actions.clickDeleteMlSnapshot(); + await actions.mlDeprecationFlyout.clickDeleteSnapshot(); const request = server.requests[server.requests.length - 1]; @@ -154,7 +184,13 @@ describe('Machine learning deprecation flyout', () => { expect(find('mlActionResolutionCell').at(0).text()).toEqual('Deletion complete'); // Reopen the flyout - await actions.clickMlDeprecationAt(0); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); + + // Flyout actions should be hidden if deprecation was resolved + expect(exists('mlSnapshotDetails.upgradeSnapshotButton')).toBe(false); + expect(exists('mlSnapshotDetails.deleteSnapshotButton')).toBe(false); + // Badge should be updated in flyout title + expect(exists('mlSnapshotDetails.resolvedDeprecationBadge')).toBe(true); }); it('handles delete failure', async () => { @@ -168,7 +204,7 @@ describe('Machine learning deprecation flyout', () => { httpRequestsMockHelpers.setDeleteMlSnapshotResponse(undefined, error); - await actions.clickDeleteMlSnapshot(); + await actions.mlDeprecationFlyout.clickDeleteSnapshot(); const request = server.requests[server.requests.length - 1]; @@ -183,7 +219,7 @@ describe('Machine learning deprecation flyout', () => { expect(find('mlActionResolutionCell').at(0).text()).toEqual('Deletion failed'); // Reopen the flyout - await actions.clickMlDeprecationAt(0); + await actions.table.clickDeprecationRowAt('mlSnapshot', 0); // Verify the flyout shows an error message expect(find('mlSnapshotDetails.resolveSnapshotError').text()).toContain( diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts index c93cdcb1f4d97..3c6fe0e5f5329 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/reindex_deprecation_flyout.test.ts @@ -7,9 +7,10 @@ import { act } from 'react-dom/test-utils'; -import { ElasticsearchTestBed, setupElasticsearchPage, setupEnvironment } from '../helpers'; - +import { setupEnvironment } from '../helpers'; +import { ElasticsearchTestBed, setupElasticsearchPage } from './es_deprecations.helpers'; import { esDeprecationsMockResponse, MOCK_SNAPSHOT_ID, MOCK_JOB_ID } from './mocked_responses'; +import { ReindexStatus, ReindexStep } from '../../../common/types'; // Note: The reindexing flyout UX is subject to change; more tests should be added here once functionality is built out describe('Reindex deprecation flyout', () => { @@ -40,11 +41,163 @@ describe('Reindex deprecation flyout', () => { const reindexDeprecation = esDeprecationsMockResponse.deprecations[3]; const { actions, find, exists } = testBed; - await actions.clickReindexDeprecationAt(0); + await actions.table.clickDeprecationRowAt('reindex', 0); expect(exists('reindexDetails')).toBe(true); expect(find('reindexDetails.flyoutTitle').text()).toContain( `Reindex ${reindexDeprecation.index}` ); }); + + it('renders error callout when reindex fails', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: null, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + + const { actions, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + httpRequestsMockHelpers.setStartReindexingResponse(undefined, { + statusCode: 404, + message: 'no such index [test]', + }); + + await actions.reindexDeprecationFlyout.clickReindexButton(); + + expect(exists('reindexDetails.reindexingFailedCallout')).toBe(true); + }); + + it('renders error callout when fetch status fails', async () => { + httpRequestsMockHelpers.setReindexStatusResponse(undefined, { + statusCode: 404, + message: 'no such index [test]', + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + + const { actions, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(exists('reindexDetails.fetchFailedCallout')).toBe(true); + }); + + describe('reindexing progress', () => { + it('has not started yet', async () => { + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing process'); + expect(exists('cancelReindexingDocumentsButton')).toBe(false); + }); + + it('has started but not yet reindexing documents', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: { + status: ReindexStatus.inProgress, + lastCompletedStep: ReindexStep.readonly, + reindexTaskPercComplete: null, + }, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 5%'); + expect(exists('cancelReindexingDocumentsButton')).toBe(false); + }); + + it('has started reindexing documents', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: { + status: ReindexStatus.inProgress, + lastCompletedStep: ReindexStep.reindexStarted, + reindexTaskPercComplete: 0.25, + }, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 31%'); + expect(exists('cancelReindexingDocumentsButton')).toBe(true); + }); + + it('has completed reindexing documents', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: { + status: ReindexStatus.inProgress, + lastCompletedStep: ReindexStep.reindexCompleted, + reindexTaskPercComplete: 1, + }, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing in progress… 95%'); + expect(exists('cancelReindexingDocumentsButton')).toBe(false); + }); + + it('has completed', async () => { + httpRequestsMockHelpers.setReindexStatusResponse({ + reindexOp: { + status: ReindexStatus.completed, + lastCompletedStep: ReindexStep.aliasCreated, + reindexTaskPercComplete: 1, + }, + warnings: [], + hasRequiredPrivileges: true, + }); + + await act(async () => { + testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); + }); + + testBed.component.update(); + const { actions, find, exists } = testBed; + + await actions.table.clickDeprecationRowAt('reindex', 0); + + expect(find('reindexChecklistTitle').text()).toEqual('Reindexing process'); + expect(exists('cancelReindexingDocumentsButton')).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts new file mode 100644 index 0000000000000..3fa6be18a9b31 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import SemVer from 'semver/classes/semver'; +import { + deprecationsServiceMock, + docLinksServiceMock, + notificationServiceMock, + applicationServiceMock, + httpServiceMock, + coreMock, + scopedHistoryMock, +} from 'src/core/public/mocks'; +import { sharePluginMock } from 'src/plugins/share/public/mocks'; + +import { apiService } from '../../../public/application/lib/api'; +import { breadcrumbService } from '../../../public/application/lib/breadcrumbs'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { cloudMock } from '../../../../../../x-pack/plugins/cloud/public/mocks'; + +const servicesMock = { + api: apiService, + breadcrumbs: breadcrumbService, + data: dataPluginMock.createStartContract(), +}; + +// We'll mock these values to avoid testing the locators themselves. +const idToUrlMap = { + SNAPSHOT_RESTORE_LOCATOR: 'snapshotAndRestoreUrl', + DISCOVER_APP_LOCATOR: 'discoverUrl', +}; +type IdKey = keyof typeof idToUrlMap; + +const stringifySearchParams = (params: Record) => { + const stringifiedParams = Object.keys(params).reduce((list, key) => { + const value = typeof params[key] === 'object' ? JSON.stringify(params[key]) : params[key]; + + return { ...list, [key]: value }; + }, {}); + + return new URLSearchParams(stringifiedParams).toString(); +}; + +const shareMock = sharePluginMock.createSetupContract(); +// @ts-expect-error This object is missing some properties that we're not using in the UI +shareMock.url.locators.get = (id: IdKey) => ({ + useUrl: (): string | undefined => idToUrlMap[id], + getUrl: (params: Record): string | undefined => + `${idToUrlMap[id]}?${stringifySearchParams(params)}`, +}); + +export const getAppContextMock = (kibanaVersion: SemVer) => ({ + isReadOnlyMode: false, + kibanaVersionInfo: { + currentMajor: kibanaVersion.major, + prevMajor: kibanaVersion.major - 1, + nextMajor: kibanaVersion.major + 1, + }, + services: { + ...servicesMock, + core: { + ...coreMock.createStart(), + http: httpServiceMock.createSetupContract(), + deprecations: deprecationsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + docLinks: docLinksServiceMock.createStartContract(), + history: scopedHistoryMock.create(), + application: applicationServiceMock.createStartContract(), + }, + }, + plugins: { + share: shareMock, + infra: undefined, + cloud: { + ...cloudMock.createSetup(), + isCloudEnabled: false, + }, + }, + clusterUpgradeState: 'isPreparingForUpgrade', + isClusterUpgradeStateError: () => {}, + handleClusterUpgradeStateError: () => {}, +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/elasticsearch.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/elasticsearch.helpers.ts deleted file mode 100644 index 86737d4925927..0000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/elasticsearch.helpers.ts +++ /dev/null @@ -1,171 +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 { act } from 'react-dom/test-utils'; - -import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; -import { EsDeprecations } from '../../../public/application/components/es_deprecations'; -import { WithAppDependencies } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: ['/es_deprecations'], - componentRoutePath: '/es_deprecations', - }, - doMountAsync: true, -}; - -export type ElasticsearchTestBed = TestBed & { - actions: ReturnType; -}; - -const createActions = (testBed: TestBed) => { - const { component, find } = testBed; - - /** - * User Actions - */ - const clickRefreshButton = async () => { - await act(async () => { - find('refreshButton').simulate('click'); - }); - - component.update(); - }; - - const clickMlDeprecationAt = async (index: number) => { - await act(async () => { - find('deprecation-mlSnapshot').at(index).simulate('click'); - }); - - component.update(); - }; - - const clickUpgradeMlSnapshot = async () => { - await act(async () => { - find('mlSnapshotDetails.upgradeSnapshotButton').simulate('click'); - }); - - component.update(); - }; - - const clickDeleteMlSnapshot = async () => { - await act(async () => { - find('mlSnapshotDetails.deleteSnapshotButton').simulate('click'); - }); - - component.update(); - }; - - const clickIndexSettingsDeprecationAt = async (index: number) => { - await act(async () => { - find('deprecation-indexSetting').at(index).simulate('click'); - }); - - component.update(); - }; - - const clickDeleteSettingsButton = async () => { - await act(async () => { - find('deleteSettingsButton').simulate('click'); - }); - - component.update(); - }; - - const clickReindexDeprecationAt = async (index: number) => { - await act(async () => { - find('deprecation-reindex').at(index).simulate('click'); - }); - - component.update(); - }; - - const clickDefaultDeprecationAt = async (index: number) => { - await act(async () => { - find('deprecation-default').at(index).simulate('click'); - }); - - component.update(); - }; - - const clickCriticalFilterButton = async () => { - await act(async () => { - // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector - find('searchBarContainer').find('.euiFilterButton').at(0).simulate('click'); - }); - - component.update(); - }; - - const clickTypeFilterDropdownAt = async (index: number) => { - await act(async () => { - // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector - find('searchBarContainer') - .find('.euiPopover') - .find('.euiFilterButton') - .at(index) - .simulate('click'); - }); - - component.update(); - }; - - const setSearchInputValue = async (searchValue: string) => { - await act(async () => { - find('searchBarContainer') - .find('input') - .simulate('keyup', { target: { value: searchValue } }); - }); - - component.update(); - }; - - const clickPaginationAt = async (index: number) => { - await act(async () => { - find(`pagination-button-${index}`).simulate('click'); - }); - - component.update(); - }; - - const clickRowsPerPageDropdown = async () => { - await act(async () => { - find('tablePaginationPopoverButton').simulate('click'); - }); - - component.update(); - }; - - return { - clickRefreshButton, - clickMlDeprecationAt, - clickUpgradeMlSnapshot, - clickDeleteMlSnapshot, - clickIndexSettingsDeprecationAt, - clickDeleteSettingsButton, - clickReindexDeprecationAt, - clickDefaultDeprecationAt, - clickCriticalFilterButton, - clickTypeFilterDropdownAt, - setSearchInputValue, - clickPaginationAt, - clickRowsPerPageDropdown, - }; -}; - -export const setup = async (overrides?: Record): Promise => { - const initTestBed = registerTestBed( - WithAppDependencies(EsDeprecations, overrides), - testBedConfig - ); - const testBed = await initTestBed(); - - return { - ...testBed, - actions: createActions(testBed), - }; -}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts index d0c93d74f31f4..7903ca58ac18a 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts @@ -6,12 +6,31 @@ */ import sinon, { SinonFakeServer } from 'sinon'; + import { API_BASE_PATH } from '../../../common/constants'; -import { ESUpgradeStatus, DeprecationLoggingStatus } from '../../../common/types'; -import { ResponseError } from '../../../public/application/lib/api'; +import { + CloudBackupStatus, + ESUpgradeStatus, + DeprecationLoggingStatus, + ResponseError, +} from '../../../common/types'; // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const setLoadCloudBackupStatusResponse = ( + response?: CloudBackupStatus, + error?: ResponseError + ) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/cloud_backup_status`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setLoadEsDeprecationsResponse = (response?: ESUpgradeStatus, error?: ResponseError) => { const status = error ? error.statusCode || 400 : 200; const body = error ? error : response; @@ -37,6 +56,30 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadDeprecationLogsCountResponse = ( + response?: { count: number }, + error?: ResponseError + ) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/deprecation_logging/count`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setDeleteLogsCacheResponse = (response?: string, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + server.respondWith('DELETE', `${API_BASE_PATH}/deprecation_logging/cache`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setUpdateDeprecationLoggingResponse = ( response?: DeprecationLoggingStatus, error?: ResponseError @@ -83,6 +126,28 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setReindexStatusResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/reindex/:indexName`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setStartReindexingResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('POST', `${API_BASE_PATH}/reindex/:indexName`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setDeleteMlSnapshotResponse = (response?: object, error?: ResponseError) => { const status = error ? error.statusCode || 400 : 200; const body = error ? error : response; @@ -94,7 +159,41 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadSystemIndicesMigrationStatus = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/system_indices_migration`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setLoadMlUpgradeModeResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/ml_upgrade_mode`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + + const setSystemIndicesMigrationResponse = (response?: object, error?: ResponseError) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('POST', `${API_BASE_PATH}/system_indices_migration`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { + setLoadCloudBackupStatusResponse, setLoadEsDeprecationsResponse, setLoadDeprecationLoggingResponse, setUpdateDeprecationLoggingResponse, @@ -102,6 +201,13 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setUpgradeMlSnapshotResponse, setDeleteMlSnapshotResponse, setUpgradeMlSnapshotStatusResponse, + setLoadDeprecationLogsCountResponse, + setLoadSystemIndicesMigrationStatus, + setSystemIndicesMigrationResponse, + setDeleteLogsCacheResponse, + setStartReindexingResponse, + setReindexStatusResponse, + setLoadMlUpgradeModeResponse, }; }; @@ -116,8 +222,19 @@ export const init = () => { const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + const setServerAsync = (isAsync: boolean, timeout: number = 200) => { + if (isAsync) { + server.autoRespond = true; + server.autoRespondAfter = 1000; + server.respondImmediately = false; + } else { + server.respondImmediately = true; + } + }; + return { server, + setServerAsync, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts index 2d3fff9d43e2c..f70bfd00e9c07 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts @@ -5,11 +5,5 @@ * 2.0. */ -export type { OverviewTestBed } from './overview.helpers'; -export { setup as setupOverviewPage } from './overview.helpers'; -export type { ElasticsearchTestBed } from './elasticsearch.helpers'; -export { setup as setupElasticsearchPage } from './elasticsearch.helpers'; -export type { KibanaTestBed } from './kibana.helpers'; -export { setup as setupKibanaPage } from './kibana.helpers'; - -export { setupEnvironment, kibanaVersion } from './setup_environment'; +export { setupEnvironment, WithAppDependencies, kibanaVersion } from './setup_environment'; +export { advanceTime } from './time_manipulation'; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/kibana.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/kibana.helpers.ts deleted file mode 100644 index 370679d7d1a71..0000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/kibana.helpers.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; -import { KibanaDeprecationsContent } from '../../../public/application/components/kibana_deprecations'; -import { WithAppDependencies } from './setup_environment'; - -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: ['/kibana_deprecations'], - componentRoutePath: '/kibana_deprecations', - }, - doMountAsync: true, -}; - -export type KibanaTestBed = TestBed & { - actions: ReturnType; -}; - -const createActions = (testBed: TestBed) => { - /** - * User Actions - */ - - const clickExpandAll = () => { - const { find } = testBed; - find('expandAll').simulate('click'); - }; - - return { - clickExpandAll, - }; -}; - -export const setup = async (overrides?: Record): Promise => { - const initTestBed = registerTestBed( - WithAppDependencies(KibanaDeprecationsContent, overrides), - testBedConfig - ); - const testBed = await initTestBed(); - - return { - ...testBed, - actions: createActions(testBed), - }; -}; - -export type KibanaTestSubjects = - | 'expandAll' - | 'noDeprecationsPrompt' - | 'kibanaPluginError' - | 'kibanaDeprecationsContent' - | 'kibanaDeprecationItem' - | 'kibanaRequestError' - | string; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/services_mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/services_mock.ts deleted file mode 100644 index 893b97c5a0ca6..0000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/services_mock.ts +++ /dev/null @@ -1,30 +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 { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; -import { discoverPluginMock } from '../../../../../../src/plugins/discover/public/mocks'; -import { applicationServiceMock } from '../../../../../../src/core/public/application/application_service.mock'; - -const discoverMock = discoverPluginMock.createStartContract(); - -export const servicesMock = { - data: dataPluginMock.createStartContract(), - application: applicationServiceMock.createStartContract(), - discover: { - ...discoverMock, - locator: { - ...discoverMock.locator, - getLocation: jest.fn(() => - Promise.resolve({ - app: '/discover', - path: 'logs', - state: {}, - }) - ), - }, - }, -}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx index fbbbc0e07853c..0e4af0b697a49 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx @@ -7,24 +7,21 @@ import React from 'react'; import axios from 'axios'; +import SemVer from 'semver/classes/semver'; +import { merge } from 'lodash'; // @ts-ignore import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import SemVer from 'semver/classes/semver'; -import { - deprecationsServiceMock, - docLinksServiceMock, - notificationServiceMock, - applicationServiceMock, -} from 'src/core/public/mocks'; -import { HttpSetup } from 'src/core/public'; -import { KibanaContextProvider } from '../../../public/shared_imports'; +import { HttpSetup } from 'src/core/public'; import { MAJOR_VERSION } from '../../../common/constants'; + +import { AuthorizationContext, Authorization, Privileges } from '../../../public/shared_imports'; import { AppContextProvider } from '../../../public/application/app_context'; import { apiService } from '../../../public/application/lib/api'; import { breadcrumbService } from '../../../public/application/lib/breadcrumbs'; import { GlobalFlyout } from '../../../public/shared_imports'; -import { servicesMock } from './services_mock'; +import { AppDependencies } from '../../../public/types'; +import { getAppContextMock } from './app_context.mock'; import { init as initHttpRequests } from './http_requests'; const { GlobalFlyoutProvider } = GlobalFlyout; @@ -33,46 +30,40 @@ const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); export const kibanaVersion = new SemVer(MAJOR_VERSION); +const createAuthorizationContextValue = (privileges: Privileges) => { + return { + isLoading: false, + privileges: privileges ?? { hasAllPrivileges: false, missingPrivileges: {} }, + } as Authorization; +}; + export const WithAppDependencies = - (Comp: any, overrides: Record = {}) => + (Comp: any, { privileges, ...overrides }: Record = {}) => (props: Record) => { apiService.setup(mockHttpClient as unknown as HttpSetup); breadcrumbService.setup(() => ''); - const contextValue = { - http: mockHttpClient as unknown as HttpSetup, - docLinks: docLinksServiceMock.createStartContract(), - kibanaVersionInfo: { - currentMajor: kibanaVersion.major, - prevMajor: kibanaVersion.major - 1, - nextMajor: kibanaVersion.major + 1, - }, - notifications: notificationServiceMock.createStartContract(), - isReadOnlyMode: false, - api: apiService, - breadcrumbs: breadcrumbService, - getUrlForApp: applicationServiceMock.createStartContract().getUrlForApp, - deprecations: deprecationsServiceMock.createStartContract(), - }; - - const { servicesOverrides, ...contextOverrides } = overrides; + const appContextMock = getAppContextMock(kibanaVersion) as unknown as AppDependencies; return ( - - + + - + ); }; export const setupEnvironment = () => { - const { server, httpRequestsMockHelpers } = initHttpRequests(); + const { server, setServerAsync, httpRequestsMockHelpers } = initHttpRequests(); return { server, + setServerAsync, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts new file mode 100644 index 0000000000000..65cec19549736 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; + +/** + * These helpers are intended to be used in conjunction with jest.useFakeTimers(). + */ + +const flushPromiseJobQueue = async () => { + // See https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function + await Promise.resolve(); +}; + +export const advanceTime = async (ms: number) => { + await act(async () => { + jest.advanceTimersByTime(ms); + await flushPromiseJobQueue(); + }); +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts deleted file mode 100644 index ffac7a14804a5..0000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts +++ /dev/null @@ -1,232 +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 { DomainDeprecationDetails } from 'kibana/public'; -import { act } from 'react-dom/test-utils'; -import { deprecationsServiceMock } from 'src/core/public/mocks'; - -import { KibanaTestBed, setupKibanaPage, setupEnvironment } from './helpers'; - -describe('Kibana deprecations', () => { - let testBed: KibanaTestBed; - const { server } = setupEnvironment(); - - afterAll(() => { - server.restore(); - }); - - describe('With deprecations', () => { - const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [ - { - title: 'mock-deprecation-title', - correctiveActions: { - manualSteps: ['Step 1', 'Step 2', 'Step 3'], - api: { - method: 'POST', - path: '/test', - }, - }, - domainId: 'test_domain', - level: 'critical', - message: 'Test deprecation message', - }, - ]; - - beforeEach(async () => { - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockReturnValue(kibanaDeprecationsMockResponse); - - testBed = await setupKibanaPage({ - deprecations: deprecationService, - }); - }); - - testBed.component.update(); - }); - - test('renders deprecations', () => { - const { exists, find } = testBed; - expect(exists('kibanaDeprecationsContent')).toBe(true); - expect(find('kibanaDeprecationItem').length).toEqual(1); - }); - - describe('manual steps modal', () => { - test('renders modal with a list of steps to fix a deprecation', async () => { - const { component, actions, exists, find } = testBed; - const deprecation = kibanaDeprecationsMockResponse[0]; - - expect(exists('kibanaDeprecationsContent')).toBe(true); - - // Open all deprecations - actions.clickExpandAll(); - - const accordionTestSubj = `${deprecation.domainId}Deprecation`; - - await act(async () => { - find(`${accordionTestSubj}.stepsButton`).simulate('click'); - }); - - component.update(); - - // We need to read the document "body" as the modal is added there and not inside - // the component DOM tree. - let modal = document.body.querySelector('[data-test-subj="stepsModal"]'); - - expect(modal).not.toBeNull(); - expect(modal!.textContent).toContain(`Resolve deprecation in '${deprecation.domainId}'`); - - const steps: NodeListOf | null = modal!.querySelectorAll( - '[data-test-subj="fixDeprecationSteps"] .euiStep' - ); - - expect(steps).not.toBe(null); - expect(steps.length).toEqual(deprecation!.correctiveActions!.manualSteps!.length); - - await act(async () => { - const closeButton: HTMLButtonElement | null = modal!.querySelector( - '[data-test-subj="closeButton"]' - ); - - closeButton!.click(); - }); - - component.update(); - - // Confirm modal closed and no longer appears in the DOM - modal = document.body.querySelector('[data-test-subj="stepsModal"]'); - expect(modal).toBe(null); - }); - }); - - describe('resolve modal', () => { - test('renders confirmation modal to resolve a deprecation', async () => { - const { component, actions, exists, find } = testBed; - const deprecation = kibanaDeprecationsMockResponse[0]; - - expect(exists('kibanaDeprecationsContent')).toBe(true); - - // Open all deprecations - actions.clickExpandAll(); - - const accordionTestSubj = `${deprecation.domainId}Deprecation`; - - await act(async () => { - find(`${accordionTestSubj}.resolveButton`).simulate('click'); - }); - - component.update(); - - // We need to read the document "body" as the modal is added there and not inside - // the component DOM tree. - let modal = document.body.querySelector('[data-test-subj="resolveModal"]'); - - expect(modal).not.toBe(null); - expect(modal!.textContent).toContain(`Resolve deprecation in '${deprecation.domainId}'`); - - const confirmButton: HTMLButtonElement | null = modal!.querySelector( - '[data-test-subj="confirmModalConfirmButton"]' - ); - - await act(async () => { - confirmButton!.click(); - }); - - component.update(); - - // Confirm modal should close and no longer appears in the DOM - modal = document.body.querySelector('[data-test-subj="resolveModal"]'); - expect(modal).toBe(null); - }); - }); - }); - - describe('No deprecations', () => { - beforeEach(async () => { - await act(async () => { - testBed = await setupKibanaPage({ isReadOnlyMode: false }); - }); - - const { component } = testBed; - - component.update(); - }); - - test('renders prompt', () => { - const { exists, find } = testBed; - expect(exists('noDeprecationsPrompt')).toBe(true); - expect(find('noDeprecationsPrompt').text()).toContain( - 'Your Kibana configuration is up to date' - ); - }); - }); - - describe('Error handling', () => { - test('handles request error', async () => { - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockRejectedValue(new Error('Internal Server Error')); - - testBed = await setupKibanaPage({ - deprecations: deprecationService, - }); - }); - - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('kibanaRequestError')).toBe(true); - expect(find('kibanaRequestError').text()).toContain('Could not retrieve Kibana deprecations'); - }); - - test('handles deprecation service error', async () => { - const domainId = 'test'; - const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [ - { - domainId, - title: `Failed to fetch deprecations for ${domainId}`, - message: `Failed to get deprecations info for plugin "${domainId}".`, - level: 'fetch_error', - correctiveActions: { - manualSteps: ['Check Kibana server logs for error message.'], - }, - }, - ]; - - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockReturnValue(kibanaDeprecationsMockResponse); - - testBed = await setupKibanaPage({ - deprecations: deprecationService, - }); - }); - - const { component, exists, find, actions } = testBed; - component.update(); - - // Verify top-level callout renders - expect(exists('kibanaPluginError')).toBe(true); - expect(find('kibanaPluginError').text()).toContain( - 'Not all Kibana deprecations were retrieved successfully' - ); - - // Open all deprecations - actions.clickExpandAll(); - - // Verify callout also displays for deprecation with error - expect(exists(`${domainId}Error`)).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts new file mode 100644 index 0000000000000..9677104a6e558 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../service.mock'; +import { KibanaTestBed, setupKibanaPage } from '../kibana_deprecations.helpers'; + +describe('Kibana deprecations - Deprecation details flyout', () => { + let testBed: KibanaTestBed; + const { server } = setupEnvironment(); + const { + defaultMockedResponses: { mockedKibanaDeprecations }, + } = kibanaDeprecationsServiceHelpers; + const deprecationService = deprecationsServiceMock.createStartContract(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + await act(async () => { + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService }); + + testBed = await setupKibanaPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + testBed.component.update(); + }); + + describe('Deprecation with manual steps', () => { + test('renders flyout with single manual step as a standalone paragraph', async () => { + const { find, exists, actions } = testBed; + const manualDeprecation = mockedKibanaDeprecations[1]; + + await actions.table.clickDeprecationAt(0); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); + expect(find('manualStep').length).toBe(1); + }); + + test('renders flyout with multiple manual steps as a list', async () => { + const { find, exists, actions } = testBed; + const manualDeprecation = mockedKibanaDeprecations[1]; + + await actions.table.clickDeprecationAt(1); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); + expect(find('manualStepsListItem').length).toBe(3); + }); + + test(`doesn't show corrective actions title and steps if there aren't any`, async () => { + const { find, exists, actions } = testBed; + const manualDeprecation = mockedKibanaDeprecations[2]; + + await actions.table.clickDeprecationAt(2); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(exists('kibanaDeprecationDetails.manualStepsTitle')).toBe(false); + expect(exists('manualStepsListItem')).toBe(false); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); + }); + }); + + test('Shows documentationUrl when present', async () => { + const { find, actions } = testBed; + const deprecation = mockedKibanaDeprecations[1]; + + await actions.table.clickDeprecationAt(1); + + expect(find('kibanaDeprecationDetails.documentationLink').props().href).toBe( + deprecation.documentationUrl + ); + }); + + describe('Deprecation with automatic resolution', () => { + test('resolves deprecation successfully', async () => { + const { find, exists, actions } = testBed; + const quickResolveDeprecation = mockedKibanaDeprecations[0]; + + await actions.table.clickDeprecationAt(0); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe( + quickResolveDeprecation.title + ); + + // Quick resolve callout and button should display + expect(exists('quickResolveCallout')).toBe(true); + expect(exists('resolveButton')).toBe(true); + + await actions.flyout.clickResolveButton(); + + // Flyout should close after button click + expect(exists('kibanaDeprecationDetails')).toBe(false); + + // Reopen the flyout + await actions.table.clickDeprecationAt(0); + + // Resolve information should not display and Quick resolve button should be disabled + expect(exists('resolveSection')).toBe(false); + expect(exists('resolveButton')).toBe(false); + // Badge should be updated in flyout title + expect(exists('kibanaDeprecationDetails.resolvedDeprecationBadge')).toBe(true); + }); + + test('handles resolve failure', async () => { + const { find, exists, actions } = testBed; + const quickResolveDeprecation = mockedKibanaDeprecations[0]; + + kibanaDeprecationsServiceHelpers.setResolveDeprecations({ + deprecationService, + status: 'fail', + }); + + await actions.table.clickDeprecationAt(0); + + expect(exists('kibanaDeprecationDetails')).toBe(true); + expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); + expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe( + quickResolveDeprecation.title + ); + + // Quick resolve callout and button should display + expect(exists('quickResolveCallout')).toBe(true); + expect(exists('resolveButton')).toBe(true); + + await actions.flyout.clickResolveButton(); + + // Flyout should close after button click + expect(exists('kibanaDeprecationDetails')).toBe(false); + + // Reopen the flyout + await actions.table.clickDeprecationAt(0); + + // Verify error displays + expect(exists('quickResolveError')).toBe(true); + // Resolve information should display and Quick resolve button should be enabled + expect(exists('resolveSection')).toBe(true); + // Badge should remain the same + expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); + expect(find('resolveButton').props().disabled).toBe(false); + expect(find('resolveButton').text()).toContain('Try again'); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts new file mode 100644 index 0000000000000..a14d6e087b017 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts @@ -0,0 +1,127 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; +import type { DeprecationsServiceStart } from 'kibana/public'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../service.mock'; +import { KibanaTestBed, setupKibanaPage } from '../kibana_deprecations.helpers'; + +describe('Kibana deprecations - Deprecations table', () => { + let testBed: KibanaTestBed; + let deprecationService: jest.Mocked; + + const { server } = setupEnvironment(); + const { + mockedKibanaDeprecations, + mockedCriticalKibanaDeprecations, + mockedWarningKibanaDeprecations, + mockedConfigKibanaDeprecations, + } = kibanaDeprecationsServiceHelpers.defaultMockedResponses; + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + deprecationService = deprecationsServiceMock.createStartContract(); + + await act(async () => { + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService }); + + testBed = await setupKibanaPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + testBed.component.update(); + }); + + test('renders deprecations', () => { + const { exists, table } = testBed; + + expect(exists('kibanaDeprecations')).toBe(true); + + const { tableCellsValues } = table.getMetaData('kibanaDeprecationsTable'); + + expect(tableCellsValues.length).toEqual(mockedKibanaDeprecations.length); + }); + + it('refreshes deprecation data', async () => { + const { actions } = testBed; + + expect(deprecationService.getAllDeprecations).toHaveBeenCalledTimes(1); + + await actions.table.clickRefreshButton(); + + expect(deprecationService.getAllDeprecations).toHaveBeenCalledTimes(2); + }); + + it('shows critical and warning deprecations count', () => { + const { find } = testBed; + + expect(find('criticalDeprecationsCount').text()).toContain( + mockedCriticalKibanaDeprecations.length + ); + expect(find('warningDeprecationsCount').text()).toContain( + mockedWarningKibanaDeprecations.length + ); + }); + + describe('Search bar', () => { + it('filters by "critical" status', async () => { + const { actions, table } = testBed; + + // Show only critical deprecations + await actions.searchBar.clickCriticalFilterButton(); + const { rows: criticalRows } = table.getMetaData('kibanaDeprecationsTable'); + expect(criticalRows.length).toEqual(mockedCriticalKibanaDeprecations.length); + + // Show all deprecations + await actions.searchBar.clickCriticalFilterButton(); + const { rows: allRows } = table.getMetaData('kibanaDeprecationsTable'); + expect(allRows.length).toEqual(mockedKibanaDeprecations.length); + }); + + it('filters by type', async () => { + const { table, actions } = testBed; + + await actions.searchBar.openTypeFilterDropdown(); + await actions.searchBar.filterByConfigType(); + + const { rows: configRows } = table.getMetaData('kibanaDeprecationsTable'); + + expect(configRows.length).toEqual(mockedConfigKibanaDeprecations.length); + }); + }); + + describe('No deprecations', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setupKibanaPage({ isReadOnlyMode: false }); + }); + + const { component } = testBed; + + component.update(); + }); + + test('renders prompt', () => { + const { exists, find } = testBed; + expect(exists('noDeprecationsPrompt')).toBe(true); + expect(find('noDeprecationsPrompt').text()).toContain( + 'Your Kibana configuration is up to date' + ); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts new file mode 100644 index 0000000000000..918ee759a0f45 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../service.mock'; +import { KibanaTestBed, setupKibanaPage } from '../kibana_deprecations.helpers'; + +describe('Kibana deprecations - Deprecations table - Error handling', () => { + let testBed: KibanaTestBed; + const { server } = setupEnvironment(); + const deprecationService = deprecationsServiceMock.createStartContract(); + + afterAll(() => { + server.restore(); + }); + + test('handles plugin errors', async () => { + await act(async () => { + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ + deprecationService, + response: [ + ...kibanaDeprecationsServiceHelpers.defaultMockedResponses.mockedKibanaDeprecations, + { + domainId: 'failed_plugin_id_1', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: `Failed to get deprecations info for plugin "failed_plugin_id".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + { + domainId: 'failed_plugin_id_1', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: `Failed to get deprecations info for plugin "failed_plugin_id".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + { + domainId: 'failed_plugin_id_2', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: `Failed to get deprecations info for plugin "failed_plugin_id".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ], + }); + + testBed = await setupKibanaPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component, exists, find } = testBed; + + component.update(); + + expect(exists('kibanaDeprecationErrors')).toBe(true); + expect(find('kibanaDeprecationErrors').text()).toContain( + 'Failed to get deprecation issues for these plugins: failed_plugin_id_1, failed_plugin_id_2.' + ); + }); + + test('handles request error', async () => { + await act(async () => { + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ + deprecationService, + mockRequestErrorMessage: 'Internal Server Error', + }); + + testBed = await setupKibanaPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component, find } = testBed; + component.update(); + expect(find('deprecationsPageLoadingError').text()).toContain( + 'Could not retrieve Kibana deprecation issues' + ); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts new file mode 100644 index 0000000000000..345a06d3d80a0 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts @@ -0,0 +1,127 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { registerTestBed, TestBed, TestBedConfig, findTestSubject } from '@kbn/test/jest'; +import { KibanaDeprecations } from '../../../public/application/components'; +import { WithAppDependencies } from '../helpers'; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: ['/kibana_deprecations'], + componentRoutePath: '/kibana_deprecations', + }, + doMountAsync: true, +}; + +export type KibanaTestBed = TestBed & { + actions: ReturnType; +}; + +const createActions = (testBed: TestBed) => { + const { component, find, table } = testBed; + + /** + * User Actions + */ + const tableActions = { + clickRefreshButton: async () => { + await act(async () => { + find('refreshButton').simulate('click'); + }); + + component.update(); + }, + + clickDeprecationAt: async (index: number) => { + const { rows } = table.getMetaData('kibanaDeprecationsTable'); + + const deprecationDetailsLink = findTestSubject( + rows[index].reactWrapper, + 'deprecationDetailsLink' + ); + + await act(async () => { + deprecationDetailsLink.simulate('click'); + }); + component.update(); + }, + }; + + const searchBarActions = { + openTypeFilterDropdown: async () => { + await act(async () => { + // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector + find('kibanaDeprecations') + .find('.euiSearchBar__filtersHolder') + .find('.euiPopover') + .find('.euiFilterButton') + .at(0) + .simulate('click'); + }); + + component.update(); + }, + + clickCriticalFilterButton: async () => { + await act(async () => { + // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector + find('kibanaDeprecations') + .find('.euiSearchBar__filtersHolder') + .find('.euiFilterButton') + .at(0) + .simulate('click'); + }); + + component.update(); + }, + + filterByConfigType: async () => { + // We need to read the document "body" as the filter dropdown options are added there and not inside + // the component DOM tree. The "Config" option is expected to be the first item. + const configTypeFilterButton: HTMLButtonElement | null = document.body.querySelector( + '.euiFilterSelect__items .euiFilterSelectItem' + ); + + await act(async () => { + configTypeFilterButton!.click(); + }); + + component.update(); + }, + }; + + const flyoutActions = { + clickResolveButton: async () => { + await act(async () => { + find('resolveButton').simulate('click'); + }); + + component.update(); + }, + }; + + return { + table: tableActions, + flyout: flyoutActions, + searchBar: searchBarActions, + }; +}; + +export const setupKibanaPage = async ( + overrides?: Record +): Promise => { + const initTestBed = registerTestBed( + WithAppDependencies(KibanaDeprecations, overrides), + testBedConfig + ); + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts new file mode 100644 index 0000000000000..6a3d376acecab --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeprecationsServiceStart, DomainDeprecationDetails } from 'kibana/public'; + +const kibanaDeprecations: DomainDeprecationDetails[] = [ + { + correctiveActions: { + // Only has one manual step. + manualSteps: ['Step 1'], + api: { + method: 'POST', + path: '/test', + }, + }, + domainId: 'test_domain_1', + level: 'critical', + title: 'Test deprecation title 1', + message: 'Test deprecation message 1', + deprecationType: 'config', + configPath: 'test', + }, + { + correctiveActions: { + // Has multiple manual steps. + manualSteps: ['Step 1', 'Step 2', 'Step 3'], + }, + domainId: 'test_domain_2', + level: 'warning', + title: 'Test deprecation title 1', + documentationUrl: 'https://', + message: 'Test deprecation message 2', + deprecationType: 'feature', + }, + { + correctiveActions: { + // Has no manual steps. + manualSteps: [], + }, + domainId: 'test_domain_3', + level: 'warning', + title: 'Test deprecation title 3', + message: 'Test deprecation message 3', + deprecationType: 'feature', + }, +]; + +const setLoadDeprecations = ({ + deprecationService, + response, + mockRequestErrorMessage, +}: { + deprecationService: jest.Mocked; + response?: DomainDeprecationDetails[]; + mockRequestErrorMessage?: string; +}) => { + const mockResponse = response ? response : kibanaDeprecations; + + if (mockRequestErrorMessage) { + return deprecationService.getAllDeprecations.mockRejectedValue( + new Error(mockRequestErrorMessage) + ); + } + + return deprecationService.getAllDeprecations.mockReturnValue(Promise.resolve(mockResponse)); +}; + +const setResolveDeprecations = ({ + deprecationService, + status, +}: { + deprecationService: jest.Mocked; + status: 'ok' | 'fail'; +}) => { + if (status === 'fail') { + return deprecationService.resolveDeprecation.mockReturnValue( + Promise.resolve({ + status, + reason: 'resolve failed', + }) + ); + } + + return deprecationService.resolveDeprecation.mockReturnValue( + Promise.resolve({ + status, + }) + ); +}; + +export const kibanaDeprecationsServiceHelpers = { + setLoadDeprecations, + setResolveDeprecations, + defaultMockedResponses: { + mockedKibanaDeprecations: kibanaDeprecations, + mockedCriticalKibanaDeprecations: kibanaDeprecations.filter( + (deprecation) => deprecation.level === 'critical' + ), + mockedWarningKibanaDeprecations: kibanaDeprecations.filter( + (deprecation) => deprecation.level === 'warning' + ), + mockedConfigKibanaDeprecations: kibanaDeprecations.filter( + (deprecation) => deprecation.deprecationType === 'config' + ), + }, +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx new file mode 100644 index 0000000000000..3dcc55adbe61d --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx @@ -0,0 +1,176 @@ +/* + * 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 { CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS } from '../../../../common/constants'; +import { setupEnvironment, advanceTime } from '../../helpers'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; + +describe('Overview - Backup Step', () => { + let testBed: OverviewTestBed; + let server: ReturnType['server']; + let setServerAsync: ReturnType['setServerAsync']; + let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers']; + + beforeEach(() => { + ({ server, setServerAsync, httpRequestsMockHelpers } = setupEnvironment()); + }); + + afterEach(() => { + server.restore(); + }); + + describe('On-prem', () => { + beforeEach(async () => { + testBed = await setupOverviewPage(); + }); + + test('Shows link to Snapshot and Restore', () => { + const { exists, find } = testBed; + expect(exists('snapshotRestoreLink')).toBe(true); + expect(find('snapshotRestoreLink').props().href).toBe('snapshotAndRestoreUrl'); + }); + + test('renders step as incomplete ', () => { + const { exists } = testBed; + expect(exists('backupStep-incomplete')).toBe(true); + }); + }); + + describe('On Cloud', () => { + const setupCloudOverviewPage = async () => + setupOverviewPage({ + plugins: { + cloud: { + isCloudEnabled: true, + deploymentUrl: 'deploymentUrl', + }, + }, + }); + + describe('initial loading state', () => { + beforeEach(async () => { + // We don't want the request to load backup status to resolve immediately. + setServerAsync(true); + testBed = await setupCloudOverviewPage(); + }); + + afterEach(() => { + setServerAsync(false); + }); + + test('is rendered', () => { + const { exists } = testBed; + expect(exists('cloudBackupLoading')).toBe(true); + }); + }); + + describe('error state', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('is rendered', () => { + const { exists } = testBed; + testBed.component.update(); + expect(exists('cloudBackupErrorCallout')).toBe(true); + }); + + test('lets the user attempt to reload backup status', () => { + const { exists } = testBed; + testBed.component.update(); + expect(exists('cloudBackupRetryButton')).toBe(true); + }); + }); + + describe('success state', () => { + describe('when data is backed up', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: true, + lastBackupTime: '2021-08-25T19:59:59.863Z', + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('renders link to Cloud backups and last backup time ', () => { + const { exists, find } = testBed; + expect(exists('dataBackedUpStatus')).toBe(true); + expect(exists('cloudSnapshotsLink')).toBe(true); + expect(find('dataBackedUpStatus').text()).toContain('Last snapshot created on'); + }); + + test('renders step as complete ', () => { + const { exists } = testBed; + expect(exists('backupStep-complete')).toBe(true); + }); + }); + + describe(`when data isn't backed up`, () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: false, + lastBackupTime: undefined, + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('renders link to Cloud backups and "not backed up" status', () => { + const { exists } = testBed; + expect(exists('dataNotBackedUpStatus')).toBe(true); + expect(exists('cloudSnapshotsLink')).toBe(true); + }); + + test('renders step as incomplete ', () => { + const { exists } = testBed; + expect(exists('backupStep-incomplete')).toBe(true); + }); + }); + }); + + describe('poll for new status', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + // First request will succeed. + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: true, + lastBackupTime: '2021-08-25T19:59:59.863Z', + }); + + testBed = await setupCloudOverviewPage(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('renders step as incomplete when a success state is followed by an error state', async () => { + const { exists } = testBed; + expect(exists('backupStep-complete')).toBe(true); + + // Second request will error. + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + // Resolve the polling timeout. + await advanceTime(CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS); + testBed.component.update(); + + expect(exists('backupStep-incomplete')).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.test.tsx deleted file mode 100644 index 3db75ba0a342d..0000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.test.tsx +++ /dev/null @@ -1,153 +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 { act } from 'react-dom/test-utils'; - -import { OverviewTestBed, setupOverviewPage, setupEnvironment } from '../../helpers'; -import { DeprecationLoggingStatus } from '../../../../common/types'; -import { DEPRECATION_LOGS_SOURCE_ID } from '../../../../common/constants'; - -const getLoggingResponse = (toggle: boolean): DeprecationLoggingStatus => ({ - isDeprecationLogIndexingEnabled: toggle, - isDeprecationLoggingEnabled: toggle, -}); - -describe('Overview - Fix deprecation logs step', () => { - let testBed: OverviewTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); - testBed = await setupOverviewPage(); - - const { component } = testBed; - component.update(); - }); - - afterAll(() => { - server.restore(); - }); - - describe('Step 1 - Toggle log writing and collecting', () => { - test('toggles deprecation logging', async () => { - const { find, actions } = testBed; - - httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse({ - isDeprecationLogIndexingEnabled: false, - isDeprecationLoggingEnabled: false, - }); - - expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(true); - - await actions.clickDeprecationToggle(); - - const latestRequest = server.requests[server.requests.length - 1]; - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ isEnabled: false }); - expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(false); - }); - - test('shows callout when only loggerDeprecation is enabled', async () => { - httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ - isDeprecationLogIndexingEnabled: false, - isDeprecationLoggingEnabled: true, - }); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { exists, component } = testBed; - - component.update(); - - expect(exists('deprecationWarningCallout')).toBe(true); - }); - - test('handles network error when updating logging state', async () => { - const error = { - statusCode: 500, - error: 'Internal server error', - message: 'Internal server error', - }; - - const { actions, exists } = testBed; - - httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(undefined, error); - - await actions.clickDeprecationToggle(); - - expect(exists('updateLoggingError')).toBe(true); - }); - - test('handles network error when fetching logging state', async () => { - const error = { - statusCode: 500, - error: 'Internal server error', - message: 'Internal server error', - }; - - httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('fetchLoggingError')).toBe(true); - }); - }); - - describe('Step 2 - Analyze logs', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ - isDeprecationLogIndexingEnabled: true, - isDeprecationLoggingEnabled: true, - }); - }); - - test('Has a link to see logs in observability app', async () => { - await act(async () => { - testBed = await setupOverviewPage({ - http: { - basePath: { - prepend: (url: string) => url, - }, - }, - }); - }); - - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('viewObserveLogs')).toBe(true); - expect(find('viewObserveLogs').props().href).toBe( - `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}` - ); - }); - - test('Has a link to see logs in discover app', async () => { - await act(async () => { - testBed = await setupOverviewPage({ - getUrlForApp: jest.fn((app, options) => { - return `${app}/${options.path}`; - }), - }); - }); - - const { exists, component, find } = testBed; - - component.update(); - - expect(exists('viewDiscoverLogs')).toBe(true); - expect(find('viewDiscoverLogs').props().href).toBe('/discover/logs'); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx new file mode 100644 index 0000000000000..e1cef64dfb20c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/elasticsearch_deprecation_issues.test.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../../kibana_deprecations/service.mock'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { + esCriticalAndWarningDeprecations, + esCriticalOnlyDeprecations, + esNoDeprecations, +} from './mock_es_issues'; + +describe('Overview - Fix deprecation issues step - Elasticsearch deprecations', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('When load succeeds', () => { + const setup = async () => { + // Set up with no Kibana deprecations. + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService, response: [] }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component } = testBed; + component.update(); + }; + + describe('when there are critical and warning issues', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esCriticalAndWarningDeprecations); + await setup(); + }); + + test('renders counts for both', () => { + const { exists, find } = testBed; + expect(exists('esStatsPanel')).toBe(true); + expect(find('esStatsPanel.warningDeprecations').text()).toContain('1'); + expect(find('esStatsPanel.criticalDeprecations').text()).toContain('1'); + }); + + test('panel links to ES deprecations page', () => { + const { component, find } = testBed; + component.update(); + expect(find('esStatsPanel').find('a').props().href).toBe('/es_deprecations'); + }); + }); + + describe('when there are critical but no warning issues', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esCriticalOnlyDeprecations); + await setup(); + }); + + test('renders a count for critical issues and success state for warning issues', () => { + const { exists, find } = testBed; + expect(exists('esStatsPanel')).toBe(true); + expect(find('esStatsPanel.criticalDeprecations').text()).toContain('1'); + expect(exists('esStatsPanel.noWarningDeprecationIssues')).toBe(true); + }); + + test('panel links to ES deprecations page', () => { + const { component, find } = testBed; + component.update(); + expect(find('esStatsPanel').find('a').props().href).toBe('/es_deprecations'); + }); + }); + + describe('when there no critical or warning issues', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esNoDeprecations); + await setup(); + }); + + test('renders a count for critical issues and success state for warning issues', () => { + const { exists } = testBed; + expect(exists('esStatsPanel')).toBe(true); + expect(exists('esStatsPanel.noDeprecationIssues')).toBe(true); + }); + + test(`panel doesn't link to ES deprecations page`, () => { + const { component, find } = testBed; + component.update(); + expect(find('esStatsPanel').find('a').length).toBe(0); + }); + }); + }); + + describe(`When there's a load error`, () => { + test('handles network failure', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe( + 'Could not retrieve Elasticsearch deprecation issues.' + ); + }); + + test('handles unauthorized error', async () => { + const error = { + statusCode: 403, + error: 'Forbidden', + message: 'Forbidden', + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe( + 'You are not authorized to view Elasticsearch deprecation issues.' + ); + }); + + test('handles partially upgraded error', async () => { + const error = { + statusCode: 426, + error: 'Upgrade required', + message: 'There are some nodes running a different version of Elasticsearch', + attributes: { + allNodesUpgraded: false, + }, + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage({ isReadOnlyMode: false }); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe( + 'Upgrade Kibana to the same version as your Elasticsearch cluster. One or more nodes in the cluster is running a different version than Kibana.' + ); + }); + + test('handles upgrade error', async () => { + const error = { + statusCode: 426, + error: 'Upgrade required', + message: 'There are some nodes running a different version of Elasticsearch', + attributes: { + allNodesUpgraded: true, + }, + }; + + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage({ isReadOnlyMode: false }); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe('All Elasticsearch nodes have been upgraded.'); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx new file mode 100644 index 0000000000000..b7c417fbfcb8d --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../../kibana_deprecations/service.mock'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { esCriticalAndWarningDeprecations, esNoDeprecations } from './mock_es_issues'; + +describe('Overview - Fix deprecation issues step', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('when there are critical issues in one panel', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esCriticalAndWarningDeprecations); + + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService, response: [] }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + testBed.component.update(); + }); + + test('renders step as incomplete', async () => { + const { exists } = testBed; + expect(exists(`fixIssuesStep-incomplete`)).toBe(true); + }); + }); + + describe('when there are no critical issues for either panel', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esNoDeprecations); + + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService, response: [] }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + testBed.component.update(); + }); + + test('renders step as complete', async () => { + const { exists } = testBed; + expect(exists(`fixIssuesStep-complete`)).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx new file mode 100644 index 0000000000000..c11a1481b68b5 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/kibana_deprecation_issues.test.tsx @@ -0,0 +1,133 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { deprecationsServiceMock } from 'src/core/public/mocks'; +import type { DomainDeprecationDetails } from 'kibana/public'; + +import { setupEnvironment } from '../../helpers'; +import { kibanaDeprecationsServiceHelpers } from '../../kibana_deprecations/service.mock'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { esNoDeprecations } from './mock_es_issues'; + +describe('Overview - Fix deprecation issues step - Kibana deprecations', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + const { mockedKibanaDeprecations, mockedCriticalKibanaDeprecations } = + kibanaDeprecationsServiceHelpers.defaultMockedResponses; + + afterAll(() => { + server.restore(); + }); + + describe('When load succeeds', () => { + const setup = async (response: DomainDeprecationDetails[]) => { + // Set up with no ES deprecations. + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esNoDeprecations); + + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService, response }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component } = testBed; + component.update(); + }; + + describe('when there are critical and warning issues', () => { + beforeEach(async () => { + await setup(mockedKibanaDeprecations); + }); + + test('renders counts for both', () => { + const { exists, find } = testBed; + + expect(exists('kibanaStatsPanel')).toBe(true); + expect(find('kibanaStatsPanel.criticalDeprecations').text()).toContain(1); + expect(find('kibanaStatsPanel.warningDeprecations').text()).toContain(2); + }); + + test('panel links to Kibana deprecations page', () => { + const { component, find } = testBed; + component.update(); + expect(find('kibanaStatsPanel').find('a').props().href).toBe('/kibana_deprecations'); + }); + }); + + describe('when there are critical but no warning issues', () => { + beforeEach(async () => { + await setup(mockedCriticalKibanaDeprecations); + }); + + test('renders a count for critical issues and success state for warning issues', () => { + const { exists, find } = testBed; + + expect(exists('kibanaStatsPanel')).toBe(true); + expect(find('kibanaStatsPanel.criticalDeprecations').text()).toContain(1); + expect(exists('kibanaStatsPanel.noWarningDeprecationIssues')).toBe(true); + }); + + test('panel links to Kibana deprecations page', () => { + const { component, find } = testBed; + component.update(); + expect(find('kibanaStatsPanel').find('a').props().href).toBe('/kibana_deprecations'); + }); + }); + + describe('when there no critical or warning issues', () => { + beforeEach(async () => { + await setup([]); + }); + + test('renders a success state for the panel', () => { + const { exists } = testBed; + expect(exists('kibanaStatsPanel')).toBe(true); + expect(exists('kibanaStatsPanel.noDeprecationIssues')).toBe(true); + }); + + test(`panel doesn't link to Kibana deprecations page`, () => { + const { component, find } = testBed; + component.update(); + expect(find('kibanaStatsPanel').find('a').length).toBe(0); + }); + }); + }); + + describe(`When there's a load error`, () => { + test('Handles network failure', async () => { + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + kibanaDeprecationsServiceHelpers.setLoadDeprecations({ + deprecationService, + mockRequestErrorMessage: 'Internal Server Error', + }); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { component, find } = testBed; + component.update(); + expect(find('loadingIssuesError').text()).toBe( + 'Could not retrieve Kibana deprecation issues.' + ); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/mock_es_issues.ts similarity index 66% rename from x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts rename to x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/mock_es_issues.ts index 57373dbf07269..13505b47c5a7f 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/mock_es_issues.ts @@ -5,10 +5,9 @@ * 2.0. */ -import type { DomainDeprecationDetails } from 'kibana/public'; import { ESUpgradeStatus } from '../../../../common/types'; -export const esDeprecations: ESUpgradeStatus = { +export const esCriticalAndWarningDeprecations: ESUpgradeStatus = { totalCriticalDeprecations: 1, deprecations: [ { @@ -33,24 +32,22 @@ export const esDeprecations: ESUpgradeStatus = { ], }; -export const esDeprecationsEmpty: ESUpgradeStatus = { +export const esCriticalOnlyDeprecations: ESUpgradeStatus = { + totalCriticalDeprecations: 1, + deprecations: [ + { + isCritical: true, + type: 'cluster_settings', + resolveDuringUpgrade: false, + message: 'Index Lifecycle Management poll interval is set too low', + url: 'https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html#ilm-poll-interval-limit', + details: + 'The Index Lifecycle Management poll interval setting [indices.lifecycle.poll_interval] is currently set to [500ms], but must be 1s or greater', + }, + ], +}; + +export const esNoDeprecations: ESUpgradeStatus = { totalCriticalDeprecations: 0, deprecations: [], }; - -export const kibanaDeprecations: DomainDeprecationDetails[] = [ - { - title: 'mock-deprecation-title', - correctiveActions: { manualSteps: ['test-step'] }, - domainId: 'xpack.spaces', - level: 'critical', - message: 'Sample warning deprecation', - }, - { - title: 'mock-deprecation-title', - correctiveActions: { manualSteps: ['test-step'] }, - domainId: 'xpack.spaces', - level: 'warning', - message: 'Sample warning deprecation', - }, -]; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx new file mode 100644 index 0000000000000..8b68f5ee449a8 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx @@ -0,0 +1,473 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +// Once the logs team register the kibana locators in their app, we should be able +// to remove this mock and follow a similar approach to how discover link is tested. +// See: https://github.com/elastic/kibana/issues/104855 +const MOCKED_TIME = '2021-09-05T10:49:01.805Z'; +jest.mock('../../../../public/application/lib/logs_checkpoint', () => { + const originalModule = jest.requireActual('../../../../public/application/lib/logs_checkpoint'); + + return { + __esModule: true, + ...originalModule, + loadLogsCheckpoint: jest.fn().mockReturnValue('2021-09-05T10:49:01.805Z'), + }; +}); + +import { DeprecationLoggingStatus } from '../../../../common/types'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { setupEnvironment, advanceTime } from '../../helpers'; +import { + DEPRECATION_LOGS_INDEX, + DEPRECATION_LOGS_SOURCE_ID, + DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, +} from '../../../../common/constants'; + +const getLoggingResponse = (toggle: boolean): DeprecationLoggingStatus => ({ + isDeprecationLogIndexingEnabled: toggle, + isDeprecationLoggingEnabled: toggle, +}); + +describe('Overview - Fix deprecation logs step', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + testBed = await setupOverviewPage(); + + const { component } = testBed; + component.update(); + }); + + afterAll(() => { + server.restore(); + }); + + describe('Step status', () => { + test(`It's complete when there are no deprecation logs since last checkpoint`, async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 0 }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`fixLogsStep-complete`)).toBe(true); + }); + + test(`It's incomplete when there are deprecation logs since last checkpoint`, async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 5 }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`fixLogsStep-incomplete`)).toBe(true); + }); + + test(`It's incomplete when log collection is disabled `, async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 0 }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { actions, exists, component } = testBed; + + component.update(); + + expect(exists(`fixLogsStep-complete`)).toBe(true); + + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(false)); + + await actions.clickDeprecationToggle(); + + expect(exists(`fixLogsStep-incomplete`)).toBe(true); + }); + }); + + describe('Step 1 - Toggle log writing and collecting', () => { + test('toggles deprecation logging', async () => { + const { find, actions } = testBed; + + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(false)); + + expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(true); + + await actions.clickDeprecationToggle(); + + const latestRequest = server.requests[server.requests.length - 1]; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ isEnabled: false }); + expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(false); + }); + + test('shows callout when only loggerDeprecation is enabled', async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ + isDeprecationLogIndexingEnabled: false, + isDeprecationLoggingEnabled: true, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('deprecationWarningCallout')).toBe(true); + }); + + test('handles network error when updating logging state', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + const { actions, exists } = testBed; + + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(undefined, error); + + await actions.clickDeprecationToggle(); + + expect(exists('updateLoggingError')).toBe(true); + }); + + test('handles network error when fetching logging state', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { component, exists } = testBed; + + component.update(); + + expect(exists('fetchLoggingError')).toBe(true); + }); + + test('It doesnt show external links and deprecations count when toggle is disabled', async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ + isDeprecationLogIndexingEnabled: false, + isDeprecationLoggingEnabled: false, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('externalLinksTitle')).toBe(false); + expect(exists('deprecationsCountTitle')).toBe(false); + expect(exists('apiCompatibilityNoteTitle')).toBe(false); + }); + }); + + describe('Step 2 - Analyze logs', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + }); + + test('Has a link to see logs in observability app', async () => { + await act(async () => { + testBed = await setupOverviewPage({ + http: { + basePath: { + prepend: (url: string) => url, + }, + }, + plugins: { + infra: {}, + }, + }); + }); + + const { component, exists, find } = testBed; + + component.update(); + + expect(exists('viewObserveLogs')).toBe(true); + expect(find('viewObserveLogs').props().href).toBe( + `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}&logPosition=(end:now,start:'${MOCKED_TIME}')` + ); + }); + + test(`Doesn't show observability app link if infra app is not available`, async () => { + const { component, exists } = testBed; + + component.update(); + + expect(exists('viewObserveLogs')).toBe(false); + }); + + test('Has a link to see logs in discover app', async () => { + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component, find } = testBed; + + component.update(); + + expect(exists('viewDiscoverLogs')).toBe(true); + + const decodedUrl = decodeURIComponent(find('viewDiscoverLogs').props().href); + expect(decodedUrl).toContain('discoverUrl'); + ['"language":"kuery"', '"query":"@timestamp+>'].forEach((param) => { + expect(decodedUrl).toContain(param); + }); + }); + }); + + describe('Step 3 - Resolve log issues', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + httpRequestsMockHelpers.setDeleteLogsCacheResponse('ok'); + }); + + test('With deprecation warnings', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 10, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { find, exists, component } = testBed; + + component.update(); + + expect(exists('hasWarningsCallout')).toBe(true); + expect(find('hasWarningsCallout').text()).toContain('10'); + }); + + test('No deprecation issues', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { find, exists, component } = testBed; + + component.update(); + + expect(exists('noWarningsCallout')).toBe(true); + expect(find('noWarningsCallout').text()).toContain('No deprecation issues'); + }); + + test('Handles errors and can retry', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse(undefined, error); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, actions, component } = testBed; + + component.update(); + + expect(exists('errorCallout')).toBe(true); + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + await actions.clickRetryButton(); + + expect(exists('noWarningsCallout')).toBe(true); + }); + + test('Allows user to reset last stored date', async () => { + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 10, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, actions, component } = testBed; + + component.update(); + + expect(exists('hasWarningsCallout')).toBe(true); + expect(exists('resetLastStoredDate')).toBe(true); + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + await actions.clickResetButton(); + + expect(exists('noWarningsCallout')).toBe(true); + }); + + test('Shows a toast if deleting cache fails', async () => { + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + + httpRequestsMockHelpers.setDeleteLogsCacheResponse(undefined, error); + // Initially we want to have the callout to have a warning state + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 10 }); + + const addDanger = jest.fn(); + await act(async () => { + testBed = await setupOverviewPage({ + services: { + core: { + notifications: { + toasts: { + addDanger, + }, + }, + }, + }, + }); + }); + + const { exists, actions, component } = testBed; + + component.update(); + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ count: 0 }); + + await actions.clickResetButton(); + + // The toast should always be shown if the delete logs cache fails. + expect(addDanger).toHaveBeenCalled(); + // Even though we changed the response of the getLogsCountResponse, when the + // deleteLogsCache fails the getLogsCount api should not be called and the + // status of the callout should remain the same it initially was. + expect(exists('hasWarningsCallout')).toBe(true); + }); + + describe('Poll for logs count', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + // First request should make the step be complete + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + testBed = await setupOverviewPage(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('renders step as incomplete when a success state is followed by an error state', async () => { + const { exists } = testBed; + + expect(exists('fixLogsStep-complete')).toBe(true); + + // second request will error + const error = { + statusCode: 500, + error: 'Internal server error', + message: 'Internal server error', + }; + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse(undefined, error); + + // Resolve the polling timeout. + await advanceTime(DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS); + testBed.component.update(); + + expect(exists('fixLogsStep-incomplete')).toBe(true); + }); + }); + }); + + describe('Step 4 - API compatibility header', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadDeprecationLoggingResponse(getLoggingResponse(true)); + }); + + test('It shows copy with compatibility api header advice', async () => { + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('apiCompatibilityNoteTitle')).toBe(true); + }); + }); + + describe('Privileges check', () => { + test(`permissions warning callout is hidden if user has the right privileges`, async () => { + const { exists } = testBed; + + // Index privileges warning callout should not be shown + expect(exists('noIndexPermissionsCallout')).toBe(false); + // Analyze logs and Resolve logs sections should be shown + expect(exists('externalLinksTitle')).toBe(true); + expect(exists('deprecationsCountTitle')).toBe(true); + }); + + test(`doesn't show analyze and resolve logs if it doesn't have the right privileges`, async () => { + await act(async () => { + testBed = await setupOverviewPage({ + privileges: { + hasAllPrivileges: false, + missingPrivileges: { + index: [DEPRECATION_LOGS_INDEX], + }, + }, + }); + }); + + const { exists, component } = testBed; + + component.update(); + + // No index privileges warning callout should be shown + expect(exists('noIndexPermissionsCallout')).toBe(true); + // Analyze logs and Resolve logs sections should be hidden + expect(exists('externalLinksTitle')).toBe(false); + expect(exists('deprecationsCountTitle')).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap new file mode 100644 index 0000000000000..2a512e8569d9f --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/__snapshots__/flyout.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Overview - Migrate system indices - Flyout shows correct features in flyout table 1`] = ` +Array [ + Array [ + "Security", + "Migration failed", + ], + Array [ + "Machine Learning", + "Migration in progress", + ], + Array [ + "Kibana", + "Migration required", + ], + Array [ + "Logstash", + "Migration complete", + ], +] +`; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.test.ts new file mode 100644 index 0000000000000..1e74a966b3933 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/flyout.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 { act } from 'react-dom/test-utils'; + +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { setupEnvironment } from '../../helpers'; +import { systemIndicesMigrationStatus } from './mocks'; + +describe('Overview - Migrate system indices - Flyout', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus(systemIndicesMigrationStatus); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + testBed.component.update(); + }); + + afterAll(() => { + server.restore(); + }); + + test('shows correct features in flyout table', async () => { + const { actions, table } = testBed; + + await actions.clickViewSystemIndicesState(); + + const { tableCellsValues } = table.getMetaData('flyoutDetails'); + + expect(tableCellsValues.length).toBe(systemIndicesMigrationStatus.features.length); + expect(tableCellsValues).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx new file mode 100644 index 0000000000000..e3f6d747deaed --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/migrate_system_indices.test.tsx @@ -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 { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from '../../helpers'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; + +describe('Overview - Migrate system indices', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + testBed = await setupOverviewPage(); + testBed.component.update(); + }); + + afterAll(() => { + server.restore(); + }); + + describe('Error state', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupOverviewPage(); + }); + + test('Is rendered', () => { + const { exists, component } = testBed; + component.update(); + + expect(exists('systemIndicesStatusErrorCallout')).toBe(true); + }); + + test('Lets the user attempt to reload migration status', async () => { + const { exists, component, actions } = testBed; + component.update(); + + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'NO_MIGRATION_NEEDED', + }); + + await actions.clickRetrySystemIndicesButton(); + + expect(exists('noMigrationNeededSection')).toBe(true); + }); + }); + + test('No migration needed', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'NO_MIGRATION_NEEDED', + }); + + testBed = await setupOverviewPage(); + + const { exists, component } = testBed; + + component.update(); + + expect(exists('noMigrationNeededSection')).toBe(true); + expect(exists('startSystemIndicesMigrationButton')).toBe(false); + expect(exists('viewSystemIndicesStateButton')).toBe(false); + }); + + test('Migration in progress', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'IN_PROGRESS', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + component.update(); + + // Start migration is disabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(true); + // But we keep view system indices CTA + expect(exists('viewSystemIndicesStateButton')).toBe(true); + }); + + describe('Migration needed', () => { + test('Initial state', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'MIGRATION_NEEDED', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + component.update(); + + // Start migration should be enabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(false); + // Same for view system indices status + expect(exists('viewSystemIndicesStateButton')).toBe(true); + }); + + test('Handles errors when migrating', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'MIGRATION_NEEDED', + }); + httpRequestsMockHelpers.setSystemIndicesMigrationResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupOverviewPage(); + + const { exists, component, find } = testBed; + + await act(async () => { + find('startSystemIndicesMigrationButton').simulate('click'); + }); + + component.update(); + + // Error is displayed + expect(exists('startSystemIndicesMigrationCalloutError')).toBe(true); + // CTA is enabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + expect(find('startSystemIndicesMigrationButton').props().disabled).toBe(false); + }); + + test('Handles errors from migration', async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'ERROR', + features: [ + { + feature_name: 'kibana', + indices: [ + { + index: '.kibana', + migration_status: 'ERROR', + failure_cause: { + error: { + type: 'mapper_parsing_exception', + }, + }, + }, + ], + }, + ], + }); + + testBed = await setupOverviewPage(); + + const { exists } = testBed; + + // Error is displayed + expect(exists('migrationFailedCallout')).toBe(true); + // CTA is enabled + expect(exists('startSystemIndicesMigrationButton')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts new file mode 100644 index 0000000000000..a810799c434e0 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/mocks.ts @@ -0,0 +1,58 @@ +/* + * 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 { SystemIndicesMigrationStatus } from '../../../../common/types'; + +export const systemIndicesMigrationStatus: SystemIndicesMigrationStatus = { + migration_status: 'MIGRATION_NEEDED', + features: [ + { + feature_name: 'security', + minimum_index_version: '7.1.1', + migration_status: 'ERROR', + indices: [ + { + index: '.security-7', + version: '7.1.1', + }, + ], + }, + { + feature_name: 'machine_learning', + minimum_index_version: '7.1.2', + migration_status: 'IN_PROGRESS', + indices: [ + { + index: '.ml-config', + version: '7.1.2', + }, + ], + }, + { + feature_name: 'kibana', + minimum_index_version: '7.1.3', + migration_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.kibana', + version: '7.1.3', + }, + ], + }, + { + feature_name: 'logstash', + minimum_index_version: '7.1.4', + migration_status: 'NO_MIGRATION_NEEDED', + indices: [ + { + index: '.logstash-config', + version: '7.1.4', + }, + ], + }, + ], +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts new file mode 100644 index 0000000000000..9eb0831c3c7a0 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/migrate_system_indices/step_completion.test.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; + +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; +import { setupEnvironment, advanceTime } from '../../helpers'; +import { SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS } from '../../../../common/constants'; + +describe('Overview - Migrate system indices - Step completion', () => { + let testBed: OverviewTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + test(`It's complete when no upgrade is needed`, async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'NO_MIGRATION_NEEDED', + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`migrateSystemIndicesStep-complete`)).toBe(true); + }); + + test(`It's incomplete when migration is needed`, async () => { + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'MIGRATION_NEEDED', + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`migrateSystemIndicesStep-incomplete`)).toBe(true); + }); + + describe('Poll for new status', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + // First request should make the step be incomplete + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'IN_PROGRESS', + }); + + testBed = await setupOverviewPage(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('renders step as complete when a upgraded needed status is followed by a no upgrade needed', async () => { + const { exists } = testBed; + + expect(exists('migrateSystemIndicesStep-incomplete')).toBe(true); + + httpRequestsMockHelpers.setLoadSystemIndicesMigrationStatus({ + migration_status: 'NO_MIGRATION_NEEDED', + }); + + // Resolve the polling timeout. + await advanceTime(SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS); + testBed.component.update(); + + expect(exists('migrateSystemIndicesStep-complete')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/overview.helpers.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts similarity index 50% rename from x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/overview.helpers.ts rename to x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts index 96e12d4806ee3..242d6893d1518 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/overview.helpers.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.helpers.ts @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; import { Overview } from '../../../public/application/components/overview'; -import { WithAppDependencies } from './setup_environment'; +import { WithAppDependencies } from '../helpers'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -18,7 +18,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -export type OverviewTestBed = TestBed & { +export type OverviewTestBed = TestBed & { actions: ReturnType; }; @@ -37,12 +37,58 @@ const createActions = (testBed: TestBed) => { component.update(); }; + const clickRetryButton = async () => { + const { find, component } = testBed; + + await act(async () => { + find('retryButton').simulate('click'); + }); + + component.update(); + }; + + const clickResetButton = async () => { + const { find, component } = testBed; + + await act(async () => { + find('resetLastStoredDate').simulate('click'); + }); + + component.update(); + }; + + const clickViewSystemIndicesState = async () => { + const { find, component } = testBed; + + await act(async () => { + find('viewSystemIndicesStateButton').simulate('click'); + }); + + component.update(); + }; + + const clickRetrySystemIndicesButton = async () => { + const { find, component } = testBed; + + await act(async () => { + find('systemIndicesStatusRetryButton').simulate('click'); + }); + + component.update(); + }; + return { clickDeprecationToggle, + clickRetryButton, + clickResetButton, + clickViewSystemIndicesState, + clickRetrySystemIndicesButton, }; }; -export const setup = async (overrides?: Record): Promise => { +export const setupOverviewPage = async ( + overrides?: Record +): Promise => { const initTestBed = registerTestBed(WithAppDependencies(Overview, overrides), testBedConfig); const testBed = await initTestBed(); @@ -51,31 +97,3 @@ export const setup = async (overrides?: Record): Promise { let testBed: OverviewTestBed; @@ -21,12 +22,11 @@ describe('Overview Page', () => { }); describe('Documentation links', () => { - test('Has a whatsNew link and it references nextMajor version', () => { + test('Has a whatsNew link and it references target version', () => { const { exists, find } = testBed; - const nextMajor = kibanaVersion.major + 1; expect(exists('whatsNewLink')).toBe(true); - expect(find('whatsNewLink').text()).toContain(`${nextMajor}.0`); + expect(find('whatsNewLink').text()).toContain('8'); }); test('Has a link for upgrade assistant in page header', () => { diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/review_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/review_logs_step.test.tsx deleted file mode 100644 index 2afffe989ed1b..0000000000000 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/review_logs_step.test.tsx +++ /dev/null @@ -1,233 +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 { act } from 'react-dom/test-utils'; -import { deprecationsServiceMock } from 'src/core/public/mocks'; - -import * as mockedResponses from './mocked_responses'; -import { OverviewTestBed, setupOverviewPage, setupEnvironment } from '../../helpers'; - -describe('Overview - Fix deprecated settings step', () => { - let testBed: OverviewTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(mockedResponses.esDeprecations); - - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockReturnValue(mockedResponses.kibanaDeprecations); - - testBed = await setupOverviewPage({ - deprecations: deprecationService, - }); - }); - - const { component } = testBed; - component.update(); - }); - - afterAll(() => { - server.restore(); - }); - - describe('ES deprecations', () => { - test('Shows deprecation warning and critical counts', () => { - const { exists, find } = testBed; - - expect(exists('esStatsPanel')).toBe(true); - expect(find('esStatsPanel.warningDeprecations').text()).toContain('1'); - expect(find('esStatsPanel.criticalDeprecations').text()).toContain('1'); - }); - - test('Handles network failure', async () => { - const error = { - statusCode: 500, - error: 'Cant retrieve deprecations error', - message: 'Cant retrieve deprecations error', - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('esRequestErrorIconTip')).toBe(true); - }); - - test('Hides deprecation counts if it doesnt have any', async () => { - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(mockedResponses.esDeprecationsEmpty); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { exists } = testBed; - - expect(exists('noDeprecationsLabel')).toBe(true); - }); - - test('Stats panel navigates to deprecations list if clicked', () => { - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('esStatsPanel')).toBe(true); - expect(find('esStatsPanel').find('a').props().href).toBe('/es_deprecations'); - }); - - describe('Renders ES errors', () => { - test('handles network failure', async () => { - const error = { - statusCode: 500, - error: 'Internal server error', - message: 'Internal server error', - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('esRequestErrorIconTip')).toBe(true); - }); - - test('handles unauthorized error', async () => { - const error = { - statusCode: 403, - error: 'Forbidden', - message: 'Forbidden', - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage(); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('unauthorizedErrorIconTip')).toBe(true); - }); - - test('handles partially upgraded error', async () => { - const error = { - statusCode: 426, - error: 'Upgrade required', - message: 'There are some nodes running a different version of Elasticsearch', - attributes: { - allNodesUpgraded: false, - }, - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage({ isReadOnlyMode: false }); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('partiallyUpgradedErrorIconTip')).toBe(true); - }); - - test('handles upgrade error', async () => { - const error = { - statusCode: 426, - error: 'Upgrade required', - message: 'There are some nodes running a different version of Elasticsearch', - attributes: { - allNodesUpgraded: true, - }, - }; - - httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error); - - await act(async () => { - testBed = await setupOverviewPage({ isReadOnlyMode: false }); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('upgradedErrorIconTip')).toBe(true); - }); - }); - }); - - describe('Kibana deprecations', () => { - test('Show deprecation warning and critical counts', () => { - const { exists, find } = testBed; - - expect(exists('kibanaStatsPanel')).toBe(true); - expect(find('kibanaStatsPanel.warningDeprecations').text()).toContain('1'); - expect(find('kibanaStatsPanel.criticalDeprecations').text()).toContain('1'); - }); - - test('Handles network failure', async () => { - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest - .fn() - .mockRejectedValue(new Error('Internal Server Error')); - - testBed = await setupOverviewPage({ - deprecations: deprecationService, - }); - }); - - const { component, exists } = testBed; - - component.update(); - - expect(exists('kibanaRequestErrorIconTip')).toBe(true); - }); - - test('Hides deprecation count if it doesnt have any', async () => { - await act(async () => { - const deprecationService = deprecationsServiceMock.createStartContract(); - deprecationService.getAllDeprecations = jest.fn().mockRejectedValue([]); - - testBed = await setupOverviewPage({ - deprecations: deprecationService, - }); - }); - - const { exists } = testBed; - - expect(exists('noDeprecationsLabel')).toBe(true); - expect(exists('kibanaStatsPanel.warningDeprecations')).toBe(false); - expect(exists('kibanaStatsPanel.criticalDeprecations')).toBe(false); - }); - - test('Stats panel navigates to deprecations list if clicked', () => { - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('kibanaStatsPanel')).toBe(true); - expect(find('kibanaStatsPanel').find('a').props().href).toBe('/kibana_deprecations'); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/upgrade_step/upgrade_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/upgrade_step/upgrade_step.test.tsx index 21daed29acaca..601ed8992aa47 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/upgrade_step/upgrade_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/upgrade_step/upgrade_step.test.tsx @@ -7,7 +7,8 @@ import { act } from 'react-dom/test-utils'; -import { OverviewTestBed, setupOverviewPage, setupEnvironment } from '../../helpers'; +import { setupEnvironment } from '../../helpers'; +import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; describe('Overview - Upgrade Step', () => { let testBed: OverviewTestBed; @@ -22,22 +23,24 @@ describe('Overview - Upgrade Step', () => { server.restore(); }); - describe('Step 3 - Upgrade stack', () => { - test('Shows link to setup upgrade docs for on-prem installations', () => { + describe('On-prem', () => { + test('Shows link to setup upgrade docs', () => { const { exists } = testBed; expect(exists('upgradeSetupDocsLink')).toBe(true); expect(exists('upgradeSetupCloudLink')).toBe(false); }); + }); - test('Shows upgrade cta and link to docs for cloud installations', async () => { + describe('On Cloud', () => { + test('Shows upgrade CTA and link to docs', async () => { await act(async () => { testBed = await setupOverviewPage({ - servicesOverrides: { + plugins: { cloud: { isCloudEnabled: true, - baseUrl: 'https://test.com', - cloudId: '1234', + deploymentUrl: + 'https://cloud.elastic.co./deployments/bfdad4ef99a24212a06d387593686d63', }, }, }); @@ -46,10 +49,12 @@ describe('Overview - Upgrade Step', () => { const { component, exists, find } = testBed; component.update(); - expect(exists('upgradeSetupCloudLink')).toBe(true); expect(exists('upgradeSetupDocsLink')).toBe(true); + expect(exists('upgradeSetupCloudLink')).toBe(true); - expect(find('upgradeSetupCloudLink').props().href).toBe('https://test.com/deployments/1234'); + expect(find('upgradeSetupCloudLink').props().href).toBe( + 'https://cloud.elastic.co./deployments/bfdad4ef99a24212a06d387593686d63?show_upgrade=true' + ); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/common/constants.ts b/x-pack/plugins/upgrade_assistant/common/constants.ts index 68a6b9e9cdb83..9f67786c85bab 100644 --- a/x-pack/plugins/upgrade_assistant/common/constants.ts +++ b/x-pack/plugins/upgrade_assistant/common/constants.ts @@ -24,6 +24,20 @@ export const indexSettingDeprecations = { export const API_BASE_PATH = '/api/upgrade_assistant'; +// Telemetry constants +export const UPGRADE_ASSISTANT_TELEMETRY = 'upgrade-assistant-telemetry'; + +/** + * This is the repository where Cloud stores its backup snapshots. + */ +export const CLOUD_SNAPSHOT_REPOSITORY = 'found-snapshots'; + export const DEPRECATION_WARNING_UPPER_LIMIT = 999999; export const DEPRECATION_LOGS_SOURCE_ID = 'deprecation_logs'; +export const DEPRECATION_LOGS_INDEX = '.logs-deprecation.elasticsearch-default'; export const DEPRECATION_LOGS_INDEX_PATTERN = '.logs-deprecation.elasticsearch-default'; + +export const CLUSTER_UPGRADE_STATUS_POLL_INTERVAL_MS = 45000; +export const CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS = 60000; +export const DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS = 15000; +export const SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS = 15000; diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index a296e158481fa..89afa05dfe222 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -8,16 +8,26 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; +export type DeprecationSource = 'Kibana' | 'Elasticsearch'; + +export type ClusterUpgradeState = 'isPreparingForUpgrade' | 'isUpgrading' | 'isUpgradeComplete'; + +export interface ResponseError { + statusCode: number; + message: string | Error; + attributes?: { + allNodesUpgraded: boolean; + }; +} + export enum ReindexStep { // Enum values are spaced out by 10 to give us room to insert steps in between. created = 0, - indexGroupServicesStopped = 10, readonly = 20, newIndexCreated = 30, reindexStarted = 40, reindexCompleted = 50, aliasCreated = 60, - indexGroupServicesStarted = 70, } export enum ReindexStatus { @@ -26,6 +36,9 @@ export enum ReindexStatus { failed, paused, cancelled, + // Used by the UI to differentiate if there was a failure retrieving + // the status from the server API + fetchFailed, } export const REINDEX_OP_TYPE = 'upgrade-assistant-reindex-operation'; @@ -109,14 +122,7 @@ export interface ReindexWarning { }; } -export enum IndexGroup { - ml = '___ML_REINDEX_LOCK___', - watcher = '___WATCHER_REINDEX_LOCK___', -} - // Telemetry types -export const UPGRADE_ASSISTANT_TYPE = 'upgrade-assistant-telemetry'; -export const UPGRADE_ASSISTANT_DOC_ID = 'upgrade-assistant-telemetry'; export type UIOpenOption = 'overview' | 'elasticsearch' | 'kibana'; export type UIReindexOption = 'close' | 'open' | 'start' | 'stop'; @@ -133,32 +139,7 @@ export interface UIReindex { stop: boolean; } -export interface UpgradeAssistantTelemetrySavedObject { - ui_open: { - overview: number; - elasticsearch: number; - kibana: number; - }; - ui_reindex: { - close: number; - open: number; - start: number; - stop: number; - }; -} - export interface UpgradeAssistantTelemetry { - ui_open: { - overview: number; - elasticsearch: number; - kibana: number; - }; - ui_reindex: { - close: number; - open: number; - start: number; - stop: number; - }; features: { deprecation_logging: { enabled: boolean; @@ -166,10 +147,6 @@ export interface UpgradeAssistantTelemetry { }; } -export interface UpgradeAssistantTelemetrySavedObjectAttributes { - [key: string]: any; -} - export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; export interface DeprecationInfo { level: MIGRATION_DEPRECATION_LEVEL; @@ -215,6 +192,11 @@ export interface EnrichedDeprecationInfo resolveDuringUpgrade: boolean; } +export interface CloudBackupStatus { + isBackedUp: boolean; + lastBackupTime?: string; +} + export interface ESUpgradeStatus { totalCriticalDeprecations: number; deprecations: EnrichedDeprecationInfo[]; @@ -247,3 +229,29 @@ export interface DeprecationLoggingStatus { isDeprecationLogIndexingEnabled: boolean; isDeprecationLoggingEnabled: boolean; } + +export type MIGRATION_STATUS = 'MIGRATION_NEEDED' | 'NO_MIGRATION_NEEDED' | 'IN_PROGRESS' | 'ERROR'; +export interface SystemIndicesMigrationFeature { + id?: string; + feature_name: string; + minimum_index_version: string; + migration_status: MIGRATION_STATUS; + indices: Array<{ + index: string; + version: string; + failure_cause?: { + error: { + type: string; + reason: string; + }; + }; + }>; +} +export interface SystemIndicesMigrationStatus { + features: SystemIndicesMigrationFeature[]; + migration_status: MIGRATION_STATUS; +} +export interface SystemIndicesMigrationStarted { + features: SystemIndicesMigrationFeature[]; + accepted: boolean; +} diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json index e66f25318a28c..41789b393b68d 100644 --- a/x-pack/plugins/upgrade_assistant/kibana.json +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -8,7 +8,7 @@ "githubTeam": "kibana-stack-management" }, "configPath": ["xpack", "upgrade_assistant"], - "requiredPlugins": ["management", "discover", "data", "licensing", "features", "infra"], - "optionalPlugins": ["usageCollection", "cloud"], - "requiredBundles": ["esUiShared", "kibanaReact"] + "requiredPlugins": ["management", "data", "licensing", "features", "share"], + "optionalPlugins": ["usageCollection", "cloud", "security", "infra"], + "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils"] } diff --git a/x-pack/plugins/upgrade_assistant/public/application/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/_index.scss deleted file mode 100644 index 841415620d691..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'components/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx index 864be6e5d996d..9ac90e5d81f48 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/app.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx @@ -5,73 +5,171 @@ * 2.0. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Router, Switch, Route, Redirect } from 'react-router-dom'; -import { I18nStart, ScopedHistory } from 'src/core/public'; -import { ApplicationStart } from 'kibana/public'; -import { GlobalFlyout } from '../shared_imports'; - -import { KibanaContextProvider } from '../shared_imports'; -import { AppServicesContext } from '../types'; -import { AppContextProvider, ContextValue, useAppContext } from './app_context'; -import { ComingSoonPrompt } from './components/coming_soon_prompt'; -import { EsDeprecations } from './components/es_deprecations'; -import { KibanaDeprecationsContent } from './components/kibana_deprecations'; -import { Overview } from './components/overview'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt, EuiPageContent, EuiLoadingSpinner } from '@elastic/eui'; +import { ScopedHistory } from 'src/core/public'; + import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; +import { API_BASE_PATH } from '../../common/constants'; +import { ClusterUpgradeState } from '../../common/types'; +import { APP_WRAPPER_CLASS, GlobalFlyout, AuthorizationProvider } from '../shared_imports'; +import { AppDependencies } from '../types'; +import { AppContextProvider, useAppContext } from './app_context'; +import { EsDeprecations, ComingSoonPrompt, KibanaDeprecations, Overview } from './components'; const { GlobalFlyoutProvider } = GlobalFlyout; -export interface AppDependencies extends ContextValue { - i18n: I18nStart; - history: ScopedHistory; - application: ApplicationStart; - services: AppServicesContext; -} -const App: React.FunctionComponent = () => { - const { isReadOnlyMode } = useAppContext(); +const AppHandlingClusterUpgradeState: React.FunctionComponent = () => { + const { + isReadOnlyMode, + services: { api }, + } = useAppContext(); + + const [clusterUpgradeState, setClusterUpradeState] = + useState('isPreparingForUpgrade'); + + useEffect(() => { + api.onClusterUpgradeStateChange((newClusterUpgradeState: ClusterUpgradeState) => { + setClusterUpradeState(newClusterUpgradeState); + }); + }, [api]); // Read-only mode will be enabled up until the last minor before the next major release if (isReadOnlyMode) { return ; } + if (clusterUpgradeState === 'isUpgrading') { + return ( + + + + + } + body={ +

+ +

+ } + data-test-subj="emptyPrompt" + /> +
+ ); + } + + if (clusterUpgradeState === 'isUpgradeComplete') { + return ( + + + + + } + body={ +

+ +

+ } + data-test-subj="emptyPrompt" + /> +
+ ); + } + return ( - + ); }; -export const AppWithRouter = ({ history }: { history: ScopedHistory }) => { +export const App = ({ history }: { history: ScopedHistory }) => { + const { + services: { api }, + } = useAppContext(); + + // Poll the API to detect when the cluster is either in the middle of + // a rolling upgrade or has completed one. We need to create two separate + // components: one to call this hook and one to handle state changes. + // This is because the implementation of this hook calls the state-change + // callbacks on every render, which will get the UI stuck in an infinite + // render loop if the same component both called the hook and handled + // the state changes it triggers. + const { isLoading, isInitialRequest } = api.useLoadClusterUpgradeStatus(); + + // Prevent flicker of the underlying UI while we wait for the status to fetch. + if (isLoading && isInitialRequest) { + return ( + + } /> + + ); + } + return ( - + ); }; -export const RootComponent = ({ - i18n, - history, - services, - application, - ...contextValue -}: AppDependencies) => { +export const RootComponent = (dependencies: AppDependencies) => { + const { + history, + core: { i18n, application, http }, + } = dependencies.services; + return ( - - - - + + + + - + - - + + ); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx index 88b5bd4721c36..8b11b20ed1853 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/app_context.tsx @@ -5,43 +5,17 @@ * 2.0. */ -import { - CoreStart, - DeprecationsServiceStart, - DocLinksStart, - HttpSetup, - NotificationsStart, -} from 'src/core/public'; import React, { createContext, useContext } from 'react'; -import { ApiService } from './lib/api'; -import { BreadcrumbService } from './lib/breadcrumbs'; +import { AppDependencies } from '../types'; -export interface KibanaVersionContext { - currentMajor: number; - prevMajor: number; - nextMajor: number; -} - -export interface ContextValue { - http: HttpSetup; - docLinks: DocLinksStart; - kibanaVersionInfo: KibanaVersionContext; - notifications: NotificationsStart; - isReadOnlyMode: boolean; - api: ApiService; - breadcrumbs: BreadcrumbService; - getUrlForApp: CoreStart['application']['getUrlForApp']; - deprecations: DeprecationsServiceStart; -} - -export const AppContext = createContext({} as any); +export const AppContext = createContext(undefined); export const AppContextProvider = ({ children, value, }: { children: React.ReactNode; - value: ContextValue; + value: AppDependencies; }) => { return {children}; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss deleted file mode 100644 index 8f900ca8dc055..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'es_deprecations/index'; -@import 'overview/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx index 14627f0b138b0..883a8675e0ce0 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/coming_soon_prompt.tsx @@ -11,7 +11,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { useAppContext } from '../app_context'; export const ComingSoonPrompt: React.FunctionComponent = () => { - const { kibanaVersionInfo, docLinks } = useAppContext(); + const { + kibanaVersionInfo, + services: { + core: { docLinks }, + }, + } = useAppContext(); + const { nextMajor, currentMajor } = kibanaVersionInfo; const { ELASTIC_WEBSITE_URL } = docLinks; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx index c7f974fab6a89..34850e6c97543 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/constants.tsx @@ -5,30 +5,8 @@ * 2.0. */ -import { IconColor } from '@elastic/eui'; -import { invert } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { DeprecationInfo } from '../../../common/types'; - -export const LEVEL_MAP: { [level: string]: number } = { - warning: 0, - critical: 1, -}; - -interface ReverseLevelMap { - [idx: number]: DeprecationInfo['level']; -} - -export const REVERSE_LEVEL_MAP: ReverseLevelMap = invert(LEVEL_MAP) as ReverseLevelMap; - -export const COLOR_MAP: { [level: string]: IconColor } = { - warning: 'default', - critical: 'danger', -}; - -export const DEPRECATIONS_PER_PAGE = 25; - export const DEPRECATION_TYPE_MAP = { cluster_settings: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.clusterDeprecationTypeLabel', @@ -49,3 +27,8 @@ export const DEPRECATION_TYPE_MAP = { defaultMessage: 'Machine Learning', }), }; + +export const PAGINATION_CONFIG = { + initialPageSize: 50, + pageSizeOptions: [50, 100, 200], +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss deleted file mode 100644 index 4865e977f5261..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'deprecation_types/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/_index.scss deleted file mode 100644 index c3e842941a250..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'reindex/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx index 439062e027650..6ec05b0c4fc99 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx @@ -17,10 +17,11 @@ import { EuiTitle, EuiText, EuiTextColor, - EuiLink, + EuiSpacer, } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; export interface DefaultDeprecationFlyoutProps { deprecation: EnrichedDeprecationInfo; @@ -38,12 +39,6 @@ const i18nTexts = { }, } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.learnMoreLinkLabel', - { - defaultMessage: 'Learn more about this deprecation', - } - ), closeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel', { @@ -61,8 +56,10 @@ export const DefaultDeprecationFlyout = ({ return ( <> + + -

{message}

+

{message}

{index && ( @@ -74,11 +71,9 @@ export const DefaultDeprecationFlyout = ({
-

{details}

+

{details}

- - {i18nTexts.learnMoreLinkLabel} - +

diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/table_row.tsx index d4bacb21238cd..e7fc1bb7772d3 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/table_row.tsx @@ -42,6 +42,7 @@ export const DefaultTableRow: React.FunctionComponent = ({ rowFieldNames, }, flyoutProps: { onClose: closeFlyout, + className: 'eui-textBreakWord', 'data-test-subj': 'defaultDeprecationDetails', 'aria-labelledby': 'defaultDeprecationDetailsFlyoutTitle', }, @@ -60,8 +61,8 @@ export const DefaultTableRow: React.FunctionComponent = ({ rowFieldNames, > setShowFlyout(true)} deprecation={deprecation} + openFlyout={() => setShowFlyout(true)} /> ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx index 1567562db53ee..a6add8cccdd2d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButton, EuiButtonEmpty, @@ -19,13 +20,18 @@ import { EuiTitle, EuiText, EuiTextColor, - EuiLink, EuiSpacer, EuiCallOut, } from '@elastic/eui'; -import { EnrichedDeprecationInfo, IndexSettingAction } from '../../../../../../common/types'; -import type { ResponseError } from '../../../../lib/api'; + +import { + EnrichedDeprecationInfo, + IndexSettingAction, + ResponseError, +} from '../../../../../../common/types'; +import { uiMetricService, UIM_INDEX_SETTINGS_DELETE_CLICK } from '../../../../lib/ui_metric'; import type { Status } from '../../../types'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; export interface RemoveIndexSettingsFlyoutProps { deprecation: EnrichedDeprecationInfo; @@ -48,12 +54,6 @@ const i18nTexts = { }, } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.removeSettingsFlyout.learnMoreLinkLabel', - { - defaultMessage: 'Learn more about this deprecation', - } - ), removeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.removeSettingsFlyout.removeButtonLabel', { @@ -106,11 +106,21 @@ export const RemoveIndexSettingsFlyout = ({ // Flag used to hide certain parts of the UI if the deprecation has been resolved or is in progress const isResolvable = ['idle', 'error'].includes(statusType); + const onRemoveSettings = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_INDEX_SETTINGS_DELETE_CLICK); + removeIndexSettings(index!, (correctiveAction as IndexSettingAction).deprecatedSettings); + }, [correctiveAction, index, removeIndexSettings]); + return ( <> + + -

{message}

+

{message}

@@ -136,9 +146,7 @@ export const RemoveIndexSettingsFlyout = ({

{details}

- - {i18nTexts.learnMoreLinkLabel} - +

@@ -184,12 +192,7 @@ export const RemoveIndexSettingsFlyout = ({ fill data-test-subj="deleteSettingsButton" color="danger" - onClick={() => - removeIndexSettings( - index!, - (correctiveAction as IndexSettingAction).deprecatedSettings - ) - } + onClick={onRemoveSettings} > {statusType === 'error' ? i18nTexts.retryRemoveButtonLabel diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/resolution_table_cell.tsx index a5a586927c811..f982e84dce6da 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/resolution_table_cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/resolution_table_cell.tsx @@ -47,7 +47,7 @@ const i18nTexts = { 'xpack.upgradeAssistant.esDeprecations.indexSettings.resolutionTooltipLabel', { defaultMessage: - 'Resolve this deprecation by removing settings from this index. This is an automated resolution.', + 'Resolve this issue by removing settings from this index. This issue can be resolved automatically.', } ), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/table_row.tsx index b118d01a2d540..28fb11334fb3d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/table_row.tsx @@ -7,10 +7,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiTableRowCell } from '@elastic/eui'; -import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { EnrichedDeprecationInfo, ResponseError } from '../../../../../../common/types'; import { GlobalFlyout } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; -import type { ResponseError } from '../../../../lib/api'; import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells'; import { DeprecationTableColumns, Status } from '../../../types'; import { IndexSettingsResolutionCell } from './resolution_table_cell'; @@ -33,7 +32,9 @@ export const IndexSettingsTableRow: React.FunctionComponent = ({ details?: ResponseError; }>({ statusType: 'idle' }); - const { api } = useAppContext(); + const { + services: { api }, + } = useAppContext(); const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); @@ -71,6 +72,7 @@ export const IndexSettingsTableRow: React.FunctionComponent = ({ }, flyoutProps: { onClose: closeFlyout, + className: 'eui-textBreakWord', 'data-test-subj': 'indexSettingsDetails', 'aria-labelledby': 'indexSettingsDetailsFlyoutTitle', }, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx index 972d640d18c5a..3a81c7f1cc8ea 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/context.tsx @@ -12,6 +12,7 @@ import { useSnapshotState, SnapshotState } from './use_snapshot_state'; export interface MlSnapshotContext { snapshotState: SnapshotState; + mlUpgradeModeEnabled: boolean; upgradeSnapshot: () => Promise; deleteSnapshot: () => Promise; } @@ -31,12 +32,14 @@ interface Props { children: React.ReactNode; snapshotId: string; jobId: string; + mlUpgradeModeEnabled: boolean; } export const MlSnapshotsStatusProvider: React.FunctionComponent = ({ api, snapshotId, jobId, + mlUpgradeModeEnabled, children, }) => { const { updateSnapshotStatus, snapshotState, upgradeSnapshot, deleteSnapshot } = useSnapshotState( @@ -57,6 +60,7 @@ export const MlSnapshotsStatusProvider: React.FunctionComponent = ({ snapshotState, upgradeSnapshot, deleteSnapshot, + mlUpgradeModeEnabled, }} > {children} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx index ba72faf2f8c3f..a5830cf1ca655 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EuiButton, @@ -24,7 +26,15 @@ import { } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; +import { + uiMetricService, + UIM_ML_SNAPSHOT_UPGRADE_CLICK, + UIM_ML_SNAPSHOT_DELETE_CLICK, +} from '../../../../lib/ui_metric'; +import { useAppContext } from '../../../../app_context'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; import { MlSnapshotContext } from './context'; +import { SnapshotState } from './use_snapshot_state'; export interface FixSnapshotsFlyoutProps extends MlSnapshotContext { deprecation: EnrichedDeprecationInfo; @@ -38,6 +48,12 @@ const i18nTexts = { defaultMessage: 'Upgrade', } ), + upgradingButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.upgradingButtonLabel', + { + defaultMessage: 'Upgrading…', + } + ), retryUpgradeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.retryUpgradeButtonLabel', { @@ -56,6 +72,12 @@ const i18nTexts = { defaultMessage: 'Delete', } ), + deletingButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.deletingButtonLabel', + { + defaultMessage: 'Deleting…', + } + ), retryDeleteButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.flyout.retryDeleteButtonLabel', { @@ -77,12 +99,62 @@ const i18nTexts = { defaultMessage: 'Error upgrading snapshot', } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.learnMoreLinkLabel', + upgradeModeEnabledErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.upgradeModeEnabledErrorTitle', { - defaultMessage: 'Learn more about this deprecation', + defaultMessage: 'Machine Learning upgrade mode is enabled', } ), + upgradeModeEnabledErrorDescription: (docsLink: string) => ( + + + + ), + }} + /> + ), +}; + +const getDeleteButtonLabel = (snapshotState: SnapshotState) => { + if (snapshotState.action === 'delete') { + if (snapshotState.error) { + return i18nTexts.retryDeleteButtonLabel; + } + + switch (snapshotState.status) { + case 'in_progress': + return i18nTexts.deletingButtonLabel; + case 'idle': + default: + return i18nTexts.deleteButtonLabel; + } + } + return i18nTexts.deleteButtonLabel; +}; + +const getUpgradeButtonLabel = (snapshotState: SnapshotState) => { + if (snapshotState.action === 'upgrade') { + if (snapshotState.error) { + return i18nTexts.retryUpgradeButtonLabel; + } + + switch (snapshotState.status) { + case 'in_progress': + return i18nTexts.upgradingButtonLabel; + case 'idle': + default: + return i18nTexts.upgradeButtonLabel; + } + } + return i18nTexts.upgradeButtonLabel; }; export const FixSnapshotsFlyout = ({ @@ -91,16 +163,23 @@ export const FixSnapshotsFlyout = ({ snapshotState, upgradeSnapshot, deleteSnapshot, + mlUpgradeModeEnabled, }: FixSnapshotsFlyoutProps) => { - // Flag used to hide certain parts of the UI if the deprecation has been resolved or is in progress - const isResolvable = ['idle', 'error'].includes(snapshotState.status); + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + const isResolved = snapshotState.status === 'complete'; const onUpgradeSnapshot = () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_ML_SNAPSHOT_UPGRADE_CLICK); upgradeSnapshot(); closeFlyout(); }; const onDeleteSnapshot = () => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_ML_SNAPSHOT_DELETE_CLICK); deleteSnapshot(); closeFlyout(); }; @@ -108,12 +187,14 @@ export const FixSnapshotsFlyout = ({ return ( <> + + -

{i18nTexts.flyoutTitle}

+

{i18nTexts.flyoutTitle}

- {snapshotState.error && ( + {snapshotState.error && !isResolved && ( <> )} + + {mlUpgradeModeEnabled && ( + <> + +

+ {i18nTexts.upgradeModeEnabledErrorDescription(docLinks.links.ml.setUpgradeMode)} +

+
+ + + )} +

{deprecation.details}

- - {i18nTexts.learnMoreLinkLabel} - +

@@ -147,7 +243,7 @@ export const FixSnapshotsFlyout = ({ - {isResolvable && ( + {!isResolved && !mlUpgradeModeEnabled && ( @@ -155,23 +251,25 @@ export const FixSnapshotsFlyout = ({ data-test-subj="deleteSnapshotButton" color="danger" onClick={onDeleteSnapshot} - isLoading={false} + isLoading={ + snapshotState.action === 'delete' && snapshotState.status === 'in_progress' + } + isDisabled={snapshotState.status === 'in_progress'} > - {snapshotState.action === 'delete' && snapshotState.error - ? i18nTexts.retryDeleteButtonLabel - : i18nTexts.deleteButtonLabel} + {getDeleteButtonLabel(snapshotState)} - {snapshotState.action === 'upgrade' && snapshotState.error - ? i18nTexts.retryUpgradeButtonLabel - : i18nTexts.upgradeButtonLabel} + {getUpgradeButtonLabel(snapshotState)} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/resolution_table_cell.tsx index 7963701b5c543..1c3e23d0b6ca6 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/resolution_table_cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/resolution_table_cell.tsx @@ -66,7 +66,7 @@ const i18nTexts = { 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.resolutionTooltipLabel', { defaultMessage: - 'Resolve this deprecation by upgrading or deleting a job model snapshot. This is an automated resolution.', + 'Resolve this issue by upgrading or deleting a job model snapshot. This issue can be resolved automatically.', } ), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx index 9d961aed8ffc9..37dddd8171c83 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/table_row.tsx @@ -21,6 +21,7 @@ const { useGlobalFlyout } = GlobalFlyout; interface TableRowProps { deprecation: EnrichedDeprecationInfo; rowFieldNames: DeprecationTableColumns[]; + mlUpgradeModeEnabled: boolean; } export const MlSnapshotsTableRowCells: React.FunctionComponent = ({ @@ -50,6 +51,7 @@ export const MlSnapshotsTableRowCells: React.FunctionComponent = }, flyoutProps: { onClose: closeFlyout, + className: 'eui-textBreakWord', 'data-test-subj': 'mlSnapshotDetails', 'aria-labelledby': 'mlSnapshotDetailsFlyoutTitle', }, @@ -76,12 +78,15 @@ export const MlSnapshotsTableRowCells: React.FunctionComponent = }; export const MlSnapshotsTableRow: React.FunctionComponent = (props) => { - const { api } = useAppContext(); + const { + services: { api }, + } = useAppContext(); return ( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/use_snapshot_state.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/use_snapshot_state.tsx index a724922563e05..6725ba098e3c9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/use_snapshot_state.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/use_snapshot_state.tsx @@ -7,7 +7,8 @@ import { useRef, useCallback, useState, useEffect } from 'react'; -import { ApiService, ResponseError } from '../../../../lib/api'; +import { ResponseError } from '../../../../../../common/types'; +import { ApiService } from '../../../../lib/api'; import { Status } from '../../../types'; const POLL_INTERVAL_MS = 1000; @@ -68,7 +69,7 @@ export const useSnapshotState = ({ return; } - setSnapshotState(data); + setSnapshotState({ ...data, action: 'upgrade' }); // Only keep polling if it exists and is in progress. if (data?.status === 'in_progress') { @@ -97,7 +98,7 @@ export const useSnapshotState = ({ return; } - setSnapshotState(data); + setSnapshotState({ ...data, action: 'upgrade' }); updateSnapshotStatus(); }, [api, jobId, snapshotId, updateSnapshotStatus]); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/_index.scss deleted file mode 100644 index 4cd55614ab4e6..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'flyout/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap index 9357e7d2d9b6c..f3a1723c9c6ea 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/checklist_step.test.tsx.snap @@ -3,44 +3,32 @@ exports[`ChecklistFlyout renders 1`] = ` - - } - > +

+ Learn more + , + } + } />

-
+
- -

- -

-
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap index d085e5ecc20ed..2f68d35b67505 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/__snapshots__/warning_step.test.tsx.snap @@ -2,28 +2,7 @@ exports[`WarningsFlyoutStep renders 1`] = ` - - - } - > -

- -

-
- -
+ @@ -47,13 +26,13 @@ exports[`WarningsFlyoutStep renders 1`] = ` grow={false} > diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_index.scss deleted file mode 100644 index 1c9fd599b13a8..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'step_progress'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss index a754541c2ff83..4d8ee5def30eb 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/_step_progress.scss @@ -18,7 +18,7 @@ $stepStatusToCallOutColor: ( failed: 'danger', complete: 'success', paused: 'warning', - cancelled: 'danger', + cancelled: 'warning', ); .upgStepProgress__status--circle { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx index a3a0f15188fca..705b4aa906bff 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.test.tsx @@ -14,6 +14,24 @@ import { LoadingState } from '../../../../types'; import type { ReindexState } from '../use_reindex_state'; import { ChecklistFlyoutStep } from './checklist_step'; +jest.mock('../../../../../app_context', () => { + const { docLinksServiceMock } = jest.requireActual( + '../../../../../../../../../../src/core/public/doc_links/doc_links_service.mock' + ); + + return { + useAppContext: () => { + return { + services: { + core: { + docLinks: docLinksServiceMock.createStartContract(), + }, + }, + }; + }, + }; +}); + describe('ChecklistFlyout', () => { const defaultProps = { indexName: 'myIndex', @@ -22,6 +40,11 @@ describe('ChecklistFlyout', () => { onConfirmInputChange: jest.fn(), startReindex: jest.fn(), cancelReindex: jest.fn(), + http: { + basePath: { + prepend: jest.fn(), + }, + } as any, renderGlobalCallouts: jest.fn(), reindexState: { loadingState: LoadingState.Success, @@ -45,11 +68,35 @@ describe('ChecklistFlyout', () => { expect((wrapper.find('EuiButton').props() as any).isLoading).toBe(true); }); - it('disables button if hasRequiredPrivileges is false', () => { + it('hides button if hasRequiredPrivileges is false', () => { const props = cloneDeep(defaultProps); props.reindexState.hasRequiredPrivileges = false; const wrapper = shallow(); - expect(wrapper.find('EuiButton').props().disabled).toBe(true); + expect(wrapper.exists('EuiButton')).toBe(false); + }); + + it('hides button if has error', () => { + const props = cloneDeep(defaultProps); + props.reindexState.status = ReindexStatus.fetchFailed; + props.reindexState.errorMessage = 'Index not found'; + const wrapper = shallow(); + expect(wrapper.exists('EuiButton')).toBe(false); + }); + + it('shows get status error callout', () => { + const props = cloneDeep(defaultProps); + props.reindexState.status = ReindexStatus.fetchFailed; + props.reindexState.errorMessage = 'Index not found'; + const wrapper = shallow(); + expect(wrapper.exists('[data-test-subj="fetchFailedCallout"]')).toBe(true); + }); + + it('shows reindexing callout', () => { + const props = cloneDeep(defaultProps); + props.reindexState.status = ReindexStatus.failed; + props.reindexState.errorMessage = 'Index not found'; + const wrapper = shallow(); + expect(wrapper.exists('[data-test-subj="reindexingFailedCallout"]')).toBe(true); }); it('calls startReindex when button is clicked', () => { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx index 856e2a57649df..e0b9b25d73235 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/checklist_step.tsx @@ -15,15 +15,18 @@ import { EuiFlexItem, EuiFlyoutBody, EuiFlyoutFooter, + EuiLink, EuiSpacer, - EuiTitle, + EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { ReindexStatus } from '../../../../../../../common/types'; import { LoadingState } from '../../../../types'; import type { ReindexState } from '../use_reindex_state'; import { ReindexProgress } from './progress'; +import { useAppContext } from '../../../../../app_context'; const buttonLabel = (status?: ReindexStatus) => { switch (status) { @@ -41,25 +44,25 @@ const buttonLabel = (status?: ReindexStatus) => { defaultMessage="Reindexing…" /> ); - case ReindexStatus.completed: + case ReindexStatus.paused: return ( ); - case ReindexStatus.paused: + case ReindexStatus.cancelled: return ( ); default: return ( ); } @@ -69,45 +72,27 @@ const buttonLabel = (status?: ReindexStatus) => { * Displays a flyout that shows the current reindexing status for a given index. */ export const ChecklistFlyoutStep: React.FunctionComponent<{ - renderGlobalCallouts: () => React.ReactNode; closeFlyout: () => void; reindexState: ReindexState; startReindex: () => void; cancelReindex: () => void; -}> = ({ closeFlyout, reindexState, startReindex, cancelReindex, renderGlobalCallouts }) => { +}> = ({ closeFlyout, reindexState, startReindex, cancelReindex }) => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + const { loadingState, status, hasRequiredPrivileges } = reindexState; const loading = loadingState === LoadingState.Loading || status === ReindexStatus.inProgress; + const isCompleted = status === ReindexStatus.completed; + const hasFetchFailed = status === ReindexStatus.fetchFailed; + const hasReindexingFailed = status === ReindexStatus.failed; return ( - {renderGlobalCallouts()} - - } - color="warning" - iconType="alert" - > -

- -

-

- -

-
- {!hasRequiredPrivileges && ( + {hasRequiredPrivileges === false && ( )} - - -

+ {(hasFetchFailed || hasReindexingFailed) && ( + <> + + + ) : ( + + ) + } + > + {reindexState.errorMessage} + + + )} + +

+ + {i18n.translate( + 'xpack.upgradeAssistant.checkupTab.reindexing.flyout.learnMoreLinkLabel', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> +

+

-

-
+

+ +
@@ -143,18 +171,21 @@ export const ChecklistFlyoutStep: React.FunctionComponent<{ />
- - - {buttonLabel(status)} - - + {!hasFetchFailed && !isCompleted && hasRequiredPrivileges && ( + + + {buttonLabel(status)} + + + )}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx index f10e7b4cc687e..82d0f57c22a55 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/container.tsx @@ -5,74 +5,28 @@ * 2.0. */ -import React, { useState } from 'react'; -import { DocLinksStart } from 'kibana/public'; -import { i18n } from '@kbn/i18n'; +import React, { useCallback, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCallOut, EuiFlyoutHeader, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; -import { - EnrichedDeprecationInfo, - ReindexAction, - ReindexStatus, -} from '../../../../../../../common/types'; -import { useAppContext } from '../../../../../app_context'; +import { EnrichedDeprecationInfo, ReindexStatus } from '../../../../../../../common/types'; import type { ReindexStateContext } from '../context'; import { ChecklistFlyoutStep } from './checklist_step'; import { WarningsFlyoutStep } from './warnings_step'; - -enum ReindexFlyoutStep { - reindexWarnings, - checklist, -} +import { DeprecationBadge } from '../../../../shared'; +import { + UIM_REINDEX_START_CLICK, + UIM_REINDEX_STOP_CLICK, + uiMetricService, +} from '../../../../../lib/ui_metric'; export interface ReindexFlyoutProps extends ReindexStateContext { deprecation: EnrichedDeprecationInfo; closeFlyout: () => void; } -const getOpenAndCloseIndexDocLink = (docLinks: DocLinksStart) => ( - - {i18n.translate( - 'xpack.upgradeAssistant.checkupTab.reindexing.flyout.openAndCloseDocumentation', - { defaultMessage: 'documentation' } - )} - -); - -const getIndexClosedCallout = (docLinks: DocLinksStart) => ( - <> - -

- - {i18n.translate( - 'xpack.upgradeAssistant.checkupTab.reindexing.flyout.indexClosedCallout.calloutDetails.reindexingTakesLongerEmphasis', - { defaultMessage: 'Reindexing may take longer than usual' } - )} - - ), - }} - /> -

-
- - -); - export const ReindexFlyout: React.FunctionComponent = ({ reindexState, startReindex, @@ -81,53 +35,60 @@ export const ReindexFlyout: React.FunctionComponent = ({ deprecation, }) => { const { status, reindexWarnings } = reindexState; - const { index, correctiveAction } = deprecation; - const { docLinks } = useAppContext(); - // If there are any warnings and we haven't started reindexing, show the warnings step first. - const [currentFlyoutStep, setCurrentFlyoutStep] = useState( - reindexWarnings && reindexWarnings.length > 0 && status === undefined - ? ReindexFlyoutStep.reindexWarnings - : ReindexFlyoutStep.checklist - ); + const { index } = deprecation; - let flyoutContents: React.ReactNode; + const [showWarningsStep, setShowWarningsStep] = useState(false); - const globalCallout = - (correctiveAction as ReindexAction).blockerForReindexing === 'index-closed' && - reindexState.status !== ReindexStatus.completed - ? getIndexClosedCallout(docLinks) - : undefined; - switch (currentFlyoutStep) { - case ReindexFlyoutStep.reindexWarnings: - flyoutContents = ( - globalCallout} - closeFlyout={closeFlyout} - warnings={reindexState.reindexWarnings!} - advanceNextStep={() => setCurrentFlyoutStep(ReindexFlyoutStep.checklist)} - /> - ); - break; - case ReindexFlyoutStep.checklist: - flyoutContents = ( - globalCallout} - closeFlyout={closeFlyout} - reindexState={reindexState} - startReindex={startReindex} - cancelReindex={cancelReindex} - /> - ); - break; - default: - throw new Error(`Invalid flyout step: ${currentFlyoutStep}`); - } + const onStartReindex = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_START_CLICK); + startReindex(); + }, [startReindex]); + + const onStopReindex = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_STOP_CLICK); + cancelReindex(); + }, [cancelReindex]); + + const startReindexWithWarnings = () => { + if ( + reindexWarnings && + reindexWarnings.length > 0 && + status !== ReindexStatus.inProgress && + status !== ReindexStatus.completed + ) { + setShowWarningsStep(true); + } else { + onStartReindex(); + } + }; + const flyoutContents = showWarningsStep ? ( + setShowWarningsStep(false)} + continueReindex={() => { + setShowWarningsStep(false); + onStartReindex(); + }} + /> + ) : ( + + ); return ( <> + + -

+

= ({

+ {flyoutContents} ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx index b49d816302213..1ee4cf2453bdc 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types'; +import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import type { ReindexState } from '../use_reindex_state'; import { ReindexProgress } from './progress'; @@ -29,45 +29,69 @@ describe('ReindexProgress', () => { ); expect(wrapper).toMatchInlineSnapshot(` -, - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - ] - } -/> -`); + + +

+ +

+
+ , + }, + Object { + "status": "incomplete", + "title": , + }, + Object { + "status": "incomplete", + "title": , + }, + Object { + "status": "incomplete", + "title": , + }, + ] + } + /> +
+ `); }); it('displays errors in the step that failed', () => { @@ -84,104 +108,9 @@ describe('ReindexProgress', () => { cancelReindex={jest.fn()} /> ); - - const aliasStep = wrapper.props().steps[3]; + const aliasStep = (wrapper.find('StepProgress').props() as any).steps[3]; expect(aliasStep.children.props.errorMessage).toEqual( `This is an error that happened on alias switch` ); }); - - it('shows reindexing document progress bar', () => { - const wrapper = shallow( - - ); - - const reindexStep = wrapper.props().steps[2]; - expect(reindexStep.children.type.name).toEqual('ReindexProgressBar'); - expect(reindexStep.children.props.reindexState.reindexTaskPercComplete).toEqual(0.25); - }); - - it('adds steps for index groups', () => { - const wrapper = shallow( - - ); - - expect(wrapper).toMatchInlineSnapshot(` -, - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - Object { - "status": "incomplete", - "title": , - }, - ] - } -/> -`); - }); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx index 65a790fe96691..cf32a8bb3ab65 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/progress.tsx @@ -5,22 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { ReactNode } from 'react'; -import { - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiText, -} from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexGroup, ReindexStatus, ReindexStep } from '../../../../../../../common/types'; -import { LoadingState } from '../../../../types'; +import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; +import { CancelLoadingState } from '../../../../types'; import type { ReindexState } from '../use_reindex_state'; import { StepProgress, StepProgressStep } from './step_progress'; +import { getReindexProgressLabel } from '../../../../../lib/utils'; const ErrorCallout: React.FunctionComponent<{ errorMessage: string | null }> = ({ errorMessage, @@ -39,22 +33,34 @@ const PausedCallout = () => ( /> ); -const ReindexProgressBar: React.FunctionComponent<{ +const ReindexingDocumentsStepTitle: React.FunctionComponent<{ reindexState: ReindexState; cancelReindex: () => void; -}> = ({ - reindexState: { lastCompletedStep, status, reindexTaskPercComplete, cancelLoadingState }, - cancelReindex, -}) => { - const progressBar = reindexTaskPercComplete ? ( - - ) : ( - - ); +}> = ({ reindexState: { lastCompletedStep, status, cancelLoadingState }, cancelReindex }) => { + if (status === ReindexStatus.cancelled) { + return ( + <> + + + ); + } + + // step is in progress after the new index is created and while it's not completed yet + const stepInProgress = + status === ReindexStatus.inProgress && + (lastCompletedStep === ReindexStep.newIndexCreated || + lastCompletedStep === ReindexStep.reindexStarted); + // but the reindex can only be cancelled after it has started + const showCancelLink = + status === ReindexStatus.inProgress && lastCompletedStep === ReindexStep.reindexStarted; let cancelText: React.ReactNode; switch (cancelLoadingState) { - case LoadingState.Loading: + case CancelLoadingState.Requested: + case CancelLoadingState.Loading: cancelText = ( ); break; - case LoadingState.Success: + case CancelLoadingState.Success: cancelText = ( ); break; - case LoadingState.Error: - cancelText = 'Could not cancel'; + case CancelLoadingState.Error: cancelText = ( - {progressBar} + - - {cancelText} - + {stepInProgress ? ( + + ) : ( + + )} + {showCancelLink && ( + + + {cancelText} + + + )} ); }; const orderedSteps = Object.values(ReindexStep).sort() as number[]; +const getStepTitle = (step: ReindexStep, inProgress?: boolean): ReactNode => { + if (step === ReindexStep.readonly) { + return inProgress ? ( + + ) : ( + + ); + } + if (step === ReindexStep.newIndexCreated) { + return inProgress ? ( + + ) : ( + + ); + } + if (step === ReindexStep.aliasCreated) { + return inProgress ? ( + + ) : ( + + ); + } +}; /** * Displays a list of steps in the reindex operation, the current status, a progress bar, @@ -118,48 +174,53 @@ export const ReindexProgress: React.FunctionComponent<{ reindexState: ReindexState; cancelReindex: () => void; }> = (props) => { - const { errorMessage, indexGroup, lastCompletedStep = -1, status } = props.reindexState; - const stepDetails = (thisStep: ReindexStep): Pick => { + const { + errorMessage, + lastCompletedStep = -1, + status, + reindexTaskPercComplete, + } = props.reindexState; + const getProgressStep = (thisStep: ReindexStep): StepProgressStep => { const previousStep = orderedSteps[orderedSteps.indexOf(thisStep) - 1]; if (status === ReindexStatus.failed && lastCompletedStep === previousStep) { return { + title: getStepTitle(thisStep), status: 'failed', children: , }; } else if (status === ReindexStatus.paused && lastCompletedStep === previousStep) { return { + title: getStepTitle(thisStep), status: 'paused', children: , }; } else if (status === ReindexStatus.cancelled && lastCompletedStep === previousStep) { return { + title: getStepTitle(thisStep), status: 'cancelled', }; } else if (status === undefined || lastCompletedStep < previousStep) { return { + title: getStepTitle(thisStep), status: 'incomplete', }; } else if (lastCompletedStep === previousStep) { return { + title: getStepTitle(thisStep, true), status: 'inProgress', }; } else { return { + title: getStepTitle(thisStep), status: 'complete', }; } }; - // The reindexing step is special because it combines the starting and complete statuses into a single UI - // with a progress bar. + // The reindexing step is special because it generally lasts longer and can be cancelled mid-flight const reindexingDocsStep = { - title: ( - - ), + title: , } as StepProgressStep; if ( @@ -189,82 +250,38 @@ export const ReindexProgress: React.FunctionComponent<{ lastCompletedStep === ReindexStep.reindexStarted ) { reindexingDocsStep.status = 'inProgress'; - reindexingDocsStep.children = ; } else { reindexingDocsStep.status = 'complete'; } const steps = [ - { - title: ( - - ), - ...stepDetails(ReindexStep.readonly), - }, - { - title: ( - - ), - ...stepDetails(ReindexStep.newIndexCreated), - }, + getProgressStep(ReindexStep.readonly), + getProgressStep(ReindexStep.newIndexCreated), reindexingDocsStep, - { - title: ( - - ), - ...stepDetails(ReindexStep.aliasCreated), - }, + getProgressStep(ReindexStep.aliasCreated), ]; - // If this index is part of an index group, add the approriate group services steps. - if (indexGroup === IndexGroup.ml) { - steps.unshift({ - title: ( - - ), - ...stepDetails(ReindexStep.indexGroupServicesStopped), - }); - steps.push({ - title: ( - - ), - ...stepDetails(ReindexStep.indexGroupServicesStarted), - }); - } else if (indexGroup === IndexGroup.watcher) { - steps.unshift({ - title: ( - - ), - ...stepDetails(ReindexStep.indexGroupServicesStopped), - }); - steps.push({ - title: ( - - ), - ...stepDetails(ReindexStep.indexGroupServicesStarted), - }); - } - - return ; + return ( + <> + +

+ {status === ReindexStatus.inProgress ? ( + + ) : ( + + )} +

+
+ + + ); }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx index 0973f721a5372..01b4fe4eb84fc 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/step_progress.tsx @@ -10,6 +10,8 @@ import React, { Fragment, ReactNode } from 'react'; import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import './_step_progress.scss'; + type STATUS = 'incomplete' | 'inProgress' | 'complete' | 'failed' | 'paused' | 'cancelled'; const StepStatus: React.FunctionComponent<{ status: STATUS; idx: number }> = ({ status, idx }) => { @@ -54,18 +56,14 @@ const Step: React.FunctionComponent = ({ }) => { const titleClassName = classNames('upgStepProgress__title', { // eslint-disable-next-line @typescript-eslint/naming-convention - 'upgStepProgress__title--currentStep': - status === 'inProgress' || - status === 'paused' || - status === 'failed' || - status === 'cancelled', + 'upgStepProgress__title--currentStep': status === 'inProgress', }); return (
-

{title}

+
{title}
{children &&
{children}
}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx index d2cafd69e94eb..35e4a4b0b843f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx @@ -16,11 +16,6 @@ import { MAJOR_VERSION } from '../../../../../../../common/constants'; import { idForWarning, WarningsFlyoutStep } from './warnings_step'; const kibanaVersion = new SemVer(MAJOR_VERSION); -const mockKibanaVersionInfo = { - currentMajor: kibanaVersion.major, - prevMajor: kibanaVersion.major - 1, - nextMajor: kibanaVersion.major + 1, -}; jest.mock('../../../../../app_context', () => { const { docLinksServiceMock } = jest.requireActual( @@ -30,8 +25,11 @@ jest.mock('../../../../../app_context', () => { return { useAppContext: () => { return { - docLinks: docLinksServiceMock.createStartContract(), - kibanaVersionInfo: mockKibanaVersionInfo, + services: { + core: { + docLinks: docLinksServiceMock.createStartContract(), + }, + }, }; }, }; @@ -39,10 +37,9 @@ jest.mock('../../../../../app_context', () => { describe('WarningsFlyoutStep', () => { const defaultProps = { - advanceNextStep: jest.fn(), warnings: [] as ReindexWarning[], - closeFlyout: jest.fn(), - renderGlobalCallouts: jest.fn(), + hideWarningsStep: jest.fn(), + continueReindex: jest.fn(), }; it('renders', () => { @@ -76,7 +73,7 @@ describe('WarningsFlyoutStep', () => { const button = wrapper.find('EuiButton'); button.simulate('click'); - expect(defaultPropsWithWarnings.advanceNextStep).not.toHaveBeenCalled(); + expect(defaultPropsWithWarnings.continueReindex).not.toHaveBeenCalled(); // first warning (customTypeName) wrapper.find(`input#${idForWarning(0)}`).simulate('change'); @@ -84,7 +81,7 @@ describe('WarningsFlyoutStep', () => { wrapper.find(`input#${idForWarning(1)}`).simulate('change'); button.simulate('click'); - expect(defaultPropsWithWarnings.advanceNextStep).toHaveBeenCalled(); + expect(defaultPropsWithWarnings.continueReindex).toHaveBeenCalled(); }); } }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx index a5e3260167218..904e9a5e1fec6 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step_checkbox.tsx @@ -105,7 +105,7 @@ export const CustomTypeNameWarningCheckbox: React.FunctionComponent{meta!.typeName as string}, }} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx index 4415811f6bf38..d8909d4ea039f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warnings_step.tsx @@ -16,6 +16,7 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiSpacer, + EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -40,10 +41,9 @@ const warningToComponentMap: { export const idForWarning = (id: number) => `reindexWarning-${id}`; interface WarningsConfirmationFlyoutProps { - renderGlobalCallouts: () => React.ReactNode; - closeFlyout: () => void; + hideWarningsStep: () => void; + continueReindex: () => void; warnings: ReindexWarning[]; - advanceNextStep: () => void; } /** @@ -52,11 +52,14 @@ interface WarningsConfirmationFlyoutProps { */ export const WarningsFlyoutStep: React.FunctionComponent = ({ warnings, - renderGlobalCallouts, - closeFlyout, - advanceNextStep, + hideWarningsStep, + continueReindex, }) => { - const { docLinks } = useAppContext(); + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); const { links } = docLinks; const [checkedIds, setCheckedIds] = useState( @@ -83,57 +86,66 @@ export const WarningsFlyoutStep: React.FunctionComponent - {renderGlobalCallouts()} - - - } - color="danger" - iconType="alert" - > -

- -

-
- - - - {warnings.map((warning, index) => { - const WarningCheckbox = warningToComponentMap[warning.warningType]; - return ( - - ); - })} + {warnings.length > 0 && ( + <> + + } + color="warning" + iconType="alert" + > +

+ +

+
+ + +

+ +

+
+ + {warnings.map((warning, index) => { + const WarningCheckbox = warningToComponentMap[warning.warningType]; + return ( + + ); + })} + + )}
- + - + diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx index 6ea9a0277059a..b181e666c17e2 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/resolution_table_cell.tsx @@ -17,6 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { ReindexStatus } from '../../../../../../common/types'; +import { getReindexProgressLabel } from '../../../../lib/utils'; import { LoadingState } from '../../../types'; import { useReindexContext } from './context'; @@ -45,10 +46,16 @@ const i18nTexts = { defaultMessage: 'Reindex failed', } ), + reindexFetchFailedText: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.reindex.reindexFetchFailedText', + { + defaultMessage: 'Reindex status not available', + } + ), reindexCanceledText: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.reindex.reindexCanceledText', { - defaultMessage: 'Reindex canceled', + defaultMessage: 'Reindex cancelled', } ), reindexPausedText: i18n.translate( @@ -64,7 +71,7 @@ const i18nTexts = { 'xpack.upgradeAssistant.esDeprecations.reindex.resolutionTooltipLabel', { defaultMessage: - 'Resolve this deprecation by reindexing this index. This is an automated resolution.', + 'Resolve this issue by reindexing this index. This issue can be resolved automatically.', } ), }; @@ -93,7 +100,13 @@ export const ReindexResolutionCell: React.FunctionComponent = () => { - {i18nTexts.reindexInProgressText} + + {i18nTexts.reindexInProgressText}{' '} + {getReindexProgressLabel( + reindexState.reindexTaskPercComplete, + reindexState.lastCompletedStep + )} + ); @@ -119,25 +132,25 @@ export const ReindexResolutionCell: React.FunctionComponent = () => { ); - case ReindexStatus.paused: + case ReindexStatus.fetchFailed: return ( - {i18nTexts.reindexPausedText} + {i18nTexts.reindexFetchFailedText} ); - case ReindexStatus.cancelled: + case ReindexStatus.paused: return ( - {i18nTexts.reindexCanceledText} + {i18nTexts.reindexPausedText} ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx index 1cf555b6cb340..1059720e66a59 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/table_row.tsx @@ -7,9 +7,15 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiTableRowCell } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GlobalFlyout } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; +import { + uiMetricService, + UIM_REINDEX_CLOSE_FLYOUT_CLICK, + UIM_REINDEX_OPEN_FLYOUT_CLICK, +} from '../../../../lib/ui_metric'; import { DeprecationTableColumns } from '../../../types'; import { EsDeprecationsTableCells } from '../../es_deprecations_table_cells'; import { ReindexResolutionCell } from './resolution_table_cell'; @@ -29,7 +35,6 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }) => { const [showFlyout, setShowFlyout] = useState(false); const reindexState = useReindexContext(); - const { api } = useAppContext(); const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = useGlobalFlyout(); @@ -37,8 +42,8 @@ const ReindexTableRowCells: React.FunctionComponent = ({ const closeFlyout = useCallback(async () => { removeContentFromGlobalFlyout('reindexFlyout'); setShowFlyout(false); - await api.sendReindexTelemetryData({ close: true }); - }, [api, removeContentFromGlobalFlyout]); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_CLOSE_FLYOUT_CLICK); + }, [removeContentFromGlobalFlyout]); useEffect(() => { if (showFlyout) { @@ -52,6 +57,7 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }, flyoutProps: { onClose: closeFlyout, + className: 'eui-textBreakWord', 'data-test-subj': 'reindexDetails', 'aria-labelledby': 'reindexDetailsFlyoutTitle', }, @@ -61,13 +67,9 @@ const ReindexTableRowCells: React.FunctionComponent = ({ useEffect(() => { if (showFlyout) { - async function sendTelemetry() { - await api.sendReindexTelemetryData({ open: true }); - } - - sendTelemetry(); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_REINDEX_OPEN_FLYOUT_CLICK); } - }, [showFlyout, api]); + }, [showFlyout]); return ( <> @@ -92,7 +94,9 @@ const ReindexTableRowCells: React.FunctionComponent = ({ }; export const ReindexTableRow: React.FunctionComponent = (props) => { - const { api } = useAppContext(); + const { + services: { api }, + } = useAppContext(); return ( diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx index b87a509d25a55..e3a747e6615b8 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/use_reindex_state.tsx @@ -8,39 +8,36 @@ import { useRef, useCallback, useState, useEffect } from 'react'; import { - IndexGroup, ReindexOperation, ReindexStatus, ReindexStep, ReindexWarning, } from '../../../../../../common/types'; -import { LoadingState } from '../../../types'; +import { CancelLoadingState, LoadingState } from '../../../types'; import { ApiService } from '../../../../lib/api'; const POLL_INTERVAL = 1000; export interface ReindexState { loadingState: LoadingState; - cancelLoadingState?: LoadingState; + cancelLoadingState?: CancelLoadingState; lastCompletedStep?: ReindexStep; status?: ReindexStatus; reindexTaskPercComplete: number | null; errorMessage: string | null; reindexWarnings?: ReindexWarning[]; hasRequiredPrivileges?: boolean; - indexGroup?: IndexGroup; } interface StatusResponse { warnings?: ReindexWarning[]; reindexOp?: ReindexOperation; hasRequiredPrivileges?: boolean; - indexGroup?: IndexGroup; } const getReindexState = ( reindexState: ReindexState, - { reindexOp, warnings, hasRequiredPrivileges, indexGroup }: StatusResponse + { reindexOp, warnings, hasRequiredPrivileges }: StatusResponse ) => { const newReindexState = { ...reindexState, @@ -55,10 +52,6 @@ const getReindexState = ( newReindexState.hasRequiredPrivileges = hasRequiredPrivileges; } - if (indexGroup) { - newReindexState.indexGroup = indexGroup; - } - if (reindexOp) { // Prevent the UI flickering back to inProgress after cancelling newReindexState.lastCompletedStep = reindexOp.lastCompletedStep; @@ -66,8 +59,21 @@ const getReindexState = ( newReindexState.reindexTaskPercComplete = reindexOp.reindexTaskPercComplete; newReindexState.errorMessage = reindexOp.errorMessage; - if (reindexOp.status === ReindexStatus.cancelled) { - newReindexState.cancelLoadingState = LoadingState.Success; + // if reindex cancellation was "requested" or "loading" and the reindex task is now cancelled, + // then reindex cancellation has completed, set it to "success" + if ( + (reindexState.cancelLoadingState === CancelLoadingState.Requested || + reindexState.cancelLoadingState === CancelLoadingState.Loading) && + reindexOp.status === ReindexStatus.cancelled + ) { + newReindexState.cancelLoadingState = CancelLoadingState.Success; + } else if ( + // if reindex cancellation has been requested and the reindex task is still in progress, + // then reindex cancellation has not completed yet, set it to "loading" + reindexState.cancelLoadingState === CancelLoadingState.Requested && + reindexOp.status === ReindexStatus.inProgress + ) { + newReindexState.cancelLoadingState = CancelLoadingState.Loading; } } @@ -97,75 +103,81 @@ export const useReindexStatus = ({ indexName, api }: { indexName: string; api: A const { data, error } = await api.getReindexStatus(indexName); if (error) { - setReindexState({ - ...reindexState, - loadingState: LoadingState.Error, - status: ReindexStatus.failed, + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + loadingState: LoadingState.Error, + errorMessage: error.message.toString(), + status: ReindexStatus.fetchFailed, + }; }); return; } - setReindexState(getReindexState(reindexState, data)); + setReindexState((prevValue: ReindexState) => { + return getReindexState(prevValue, data); + }); // Only keep polling if it exists and is in progress. if (data.reindexOp && data.reindexOp.status === ReindexStatus.inProgress) { pollIntervalIdRef.current = setTimeout(updateStatus, POLL_INTERVAL); } - }, [clearPollInterval, api, indexName, reindexState]); + }, [clearPollInterval, api, indexName]); const startReindex = useCallback(async () => { - const currentReindexState = { - ...reindexState, - }; - - setReindexState({ - ...currentReindexState, - // Only reset last completed step if we aren't currently paused - lastCompletedStep: - currentReindexState.status === ReindexStatus.paused - ? currentReindexState.lastCompletedStep - : undefined, - status: ReindexStatus.inProgress, - reindexTaskPercComplete: null, - errorMessage: null, - cancelLoadingState: undefined, + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + // Only reset last completed step if we aren't currently paused + lastCompletedStep: + prevValue.status === ReindexStatus.paused ? prevValue.lastCompletedStep : undefined, + status: ReindexStatus.inProgress, + reindexTaskPercComplete: null, + errorMessage: null, + cancelLoadingState: undefined, + }; }); - api.sendReindexTelemetryData({ start: true }); - - const { data, error } = await api.startReindexTask(indexName); + const { data: reindexOp, error } = await api.startReindexTask(indexName); if (error) { - setReindexState({ - ...reindexState, - loadingState: LoadingState.Error, - status: ReindexStatus.failed, + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + loadingState: LoadingState.Error, + errorMessage: error.message.toString(), + status: ReindexStatus.failed, + }; }); return; } - setReindexState(getReindexState(reindexState, data)); + setReindexState((prevValue: ReindexState) => { + return getReindexState(prevValue, { reindexOp }); + }); updateStatus(); - }, [api, indexName, reindexState, updateStatus]); + }, [api, indexName, updateStatus]); const cancelReindex = useCallback(async () => { - api.sendReindexTelemetryData({ stop: true }); + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + cancelLoadingState: CancelLoadingState.Requested, + }; + }); const { error } = await api.cancelReindexTask(indexName); - setReindexState({ - ...reindexState, - cancelLoadingState: LoadingState.Loading, - }); - if (error) { - setReindexState({ - ...reindexState, - cancelLoadingState: LoadingState.Error, + setReindexState((prevValue: ReindexState) => { + return { + ...prevValue, + cancelLoadingState: CancelLoadingState.Error, + }; }); return; } - }, [api, indexName, reindexState]); + }, [api, indexName]); useEffect(() => { isMounted.current = true; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx deleted file mode 100644 index 5e3c7a5fe6cef..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecation_errors.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiCallOut } from '@elastic/eui'; - -import { ResponseError } from '../../lib/api'; -import { getEsDeprecationError } from '../../lib/get_es_deprecation_error'; -interface Props { - error: ResponseError; -} - -export const EsDeprecationErrors: React.FunctionComponent = ({ error }) => { - const { code: errorType, message } = getEsDeprecationError(error); - - switch (errorType) { - case 'unauthorized_error': - return ( - - ); - case 'partially_upgraded_error': - return ( - - ); - case 'upgraded_error': - return ; - case 'request_error': - default: - return ( - - {error.message} - - ); - } -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx index 38367bd3cfaff..270f597cb964f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx @@ -5,60 +5,110 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { EuiPageHeader, EuiSpacer, EuiPageContent } from '@elastic/eui'; +import { EuiPageHeader, EuiSpacer, EuiPageContent, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DocLinksStart } from 'kibana/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { EnrichedDeprecationInfo } from '../../../../common/types'; import { SectionLoading } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; +import { uiMetricService, UIM_ES_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; +import { getEsDeprecationError } from '../../lib/get_es_deprecation_error'; +import { DeprecationsPageLoadingError, NoDeprecationsPrompt, DeprecationCount } from '../shared'; import { EsDeprecationsTable } from './es_deprecations_table'; -import { EsDeprecationErrors } from './es_deprecation_errors'; -import { NoDeprecationsPrompt } from '../shared'; + +const getDeprecationCountByLevel = (deprecations: EnrichedDeprecationInfo[]) => { + const criticalDeprecations: EnrichedDeprecationInfo[] = []; + const warningDeprecations: EnrichedDeprecationInfo[] = []; + + deprecations.forEach((deprecation) => { + if (deprecation.isCritical) { + criticalDeprecations.push(deprecation); + return; + } + warningDeprecations.push(deprecation); + }); + + return { + criticalDeprecations: criticalDeprecations.length, + warningDeprecations: warningDeprecations.length, + }; +}; const i18nTexts = { pageTitle: i18n.translate('xpack.upgradeAssistant.esDeprecations.pageTitle', { - defaultMessage: 'Elasticsearch deprecation warnings', + defaultMessage: 'Elasticsearch deprecation issues', }), pageDescription: i18n.translate('xpack.upgradeAssistant.esDeprecations.pageDescription', { defaultMessage: - 'You must resolve all critical issues before upgrading. Back up recommended. Make sure you have a current snapshot before modifying your configuration or reindexing.', + 'Resolve all critical issues before upgrading. Before making changes, ensure you have a current snapshot of your cluster. Indices created before 7.0 must be reindexed or removed. To start multiple reindexing tasks in a single request, use the Kibana batch reindexing API.', }), isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', { - defaultMessage: 'Loading deprecations…', + defaultMessage: 'Loading deprecation issues…', }), }; -export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { - const { api, breadcrumbs } = useAppContext(); +const getBatchReindexLink = (docLinks: DocLinksStart) => { + return ( + + {i18n.translate('xpack.upgradeAssistant.esDeprecations.batchReindexingDocsLink', { + defaultMessage: 'batch reindexing API', + })} + + ), + }} + /> + ); +}; +export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { const { - data: esDeprecations, - isLoading, - error, - resendRequest, - isInitialRequest, - } = api.useLoadEsDeprecations(); + services: { + api, + breadcrumbs, + core: { docLinks }, + }, + } = useAppContext(); + + const { data: esDeprecations, isLoading, error, resendRequest } = api.useLoadEsDeprecations(); + + const deprecationsCountByLevel: { + warningDeprecations: number; + criticalDeprecations: number; + } = useMemo( + () => getDeprecationCountByLevel(esDeprecations?.deprecations || []), + [esDeprecations?.deprecations] + ); useEffect(() => { breadcrumbs.setBreadcrumbs('esDeprecations'); }, [breadcrumbs]); useEffect(() => { - if (isLoading === false && isInitialRequest) { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - elasticsearch: true, - }); - } - - sendTelemetryData(); - } - }, [api, isLoading, isInitialRequest]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_ES_DEPRECATIONS_PAGE_LOAD); + }, []); if (error) { - return ; + return ( + + ); } if (isLoading) { @@ -82,7 +132,20 @@ export const EsDeprecations = withRouter(({ history }: RouteComponentProps) => { return (
- + + {i18nTexts.pageDescription} + {getBatchReindexLink(docLinks)} + + } + > + + diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx index f1654b2030166..3d9b554913c5b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx @@ -26,6 +26,7 @@ import { Query, } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../common/types'; +import { useAppContext } from '../../app_context'; import { MlSnapshotsTableRow, DefaultTableRow, @@ -33,7 +34,7 @@ import { ReindexTableRow, } from './deprecation_types'; import { DeprecationTableColumns } from '../types'; -import { DEPRECATION_TYPE_MAP } from '../constants'; +import { DEPRECATION_TYPE_MAP, PAGINATION_CONFIG } from '../constants'; const i18nTexts = { refreshButtonLabel: i18n.translate( @@ -99,12 +100,21 @@ const cellToLabelMap = { }; const cellTypes = Object.keys(cellToLabelMap) as DeprecationTableColumns[]; -const pageSizeOptions = [50, 100, 200]; +const pageSizeOptions = PAGINATION_CONFIG.pageSizeOptions; -const renderTableRowCells = (deprecation: EnrichedDeprecationInfo) => { +const renderTableRowCells = ( + deprecation: EnrichedDeprecationInfo, + mlUpgradeModeEnabled: boolean +) => { switch (deprecation.correctiveAction?.type) { case 'mlSnapshot': - return ; + return ( + + ); case 'indexSetting': return ; @@ -146,12 +156,19 @@ export const EsDeprecationsTable: React.FunctionComponent = ({ deprecations = [], reload, }) => { + const { + services: { api }, + } = useAppContext(); + + const { data } = api.useLoadMlUpgradeMode(); + const mlUpgradeModeEnabled = !!data?.mlUpgradeModeEnabled; + const [sortConfig, setSortConfig] = useState({ isSortAscending: true, sortField: 'isCritical', }); - const [itemsPerPage, setItemsPerPage] = useState(pageSizeOptions[0]); + const [itemsPerPage, setItemsPerPage] = useState(PAGINATION_CONFIG.initialPageSize); const [currentPageIndex, setCurrentPageIndex] = useState(0); const [searchQuery, setSearchQuery] = useState(EuiSearchBar.Query.MATCH_ALL); const [searchError, setSearchError] = useState<{ message: string } | undefined>(undefined); @@ -261,7 +278,7 @@ export const EsDeprecationsTable: React.FunctionComponent = ({ - + {Object.entries(cellToLabelMap).map(([fieldName, cell]) => { return ( @@ -291,7 +308,7 @@ export const EsDeprecationsTable: React.FunctionComponent = ({ {visibleDeprecations.map((deprecation, index) => { return ( - {renderTableRowCells(deprecation)} + {renderTableRowCells(deprecation, mlUpgradeModeEnabled)} ); })} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx index dd187f19d5e96..472ecccb4f02f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx @@ -7,10 +7,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiBadge, EuiLink } from '@elastic/eui'; +import { EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../common/types'; import { DEPRECATION_TYPE_MAP } from '../constants'; import { DeprecationTableColumns } from '../types'; +import { DeprecationBadge } from '../shared'; interface Props { resolutionTableCell?: React.ReactNode; @@ -20,10 +21,16 @@ interface Props { } const i18nTexts = { - criticalBadgeLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.defaultDeprecation.criticalBadgeLabel', + manualCellLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel', { - defaultMessage: 'Critical', + defaultMessage: 'Manual', + } + ), + manualCellTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.reindex.manualCellTooltipLabel', + { + defaultMessage: 'This issue needs to be resolved manually.', } ), }; @@ -36,11 +43,7 @@ export const EsDeprecationsTableCells: React.FunctionComponent = ({ }) => { // "Status column" if (fieldName === 'isCritical') { - if (deprecation.isCritical === true) { - return {i18nTexts.criticalBadgeLabel}; - } - - return <>{''}; + return ; } // "Issue" column @@ -66,7 +69,13 @@ export const EsDeprecationsTableCells: React.FunctionComponent = ({ return <>{resolutionTableCell}; } - return <>{''}; + return ( + + + {i18nTexts.manualCellLabel} + + + ); } // Default behavior: render value or empty string if undefined diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/index.ts new file mode 100644 index 0000000000000..8924fa2d355a1 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { KibanaDeprecations } from './kibana_deprecations'; +export { EsDeprecations } from './es_deprecations'; +export { ComingSoonPrompt } from './coming_soon_prompt'; +export { Overview } from './overview'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/_deprecation_details_flyout.scss b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/_deprecation_details_flyout.scss new file mode 100644 index 0000000000000..c877ea4b48821 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/_deprecation_details_flyout.scss @@ -0,0 +1,4 @@ +// Used to add spacing between the list of manual deprecation steps +.upgResolveStep { + margin-bottom: $euiSizeL; +} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx new file mode 100644 index 0000000000000..baf725b48e6af --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; + +import { + EuiButtonEmpty, + EuiButton, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { uiMetricService, UIM_KIBANA_QUICK_RESOLVE_CLICK } from '../../lib/ui_metric'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../shared'; +import type { DeprecationResolutionState, KibanaDeprecationDetails } from './kibana_deprecations'; + +import './_deprecation_details_flyout.scss'; + +export interface DeprecationDetailsFlyoutProps { + deprecation: KibanaDeprecationDetails; + closeFlyout: () => void; + resolveDeprecation: (deprecationDetails: KibanaDeprecationDetails) => Promise; + deprecationResolutionState?: DeprecationResolutionState; +} + +const i18nTexts = { + closeButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.closeButtonLabel', + { + defaultMessage: 'Close', + } + ), + quickResolveButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveButtonLabel', + { + defaultMessage: 'Quick resolve', + } + ), + retryQuickResolveButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.retryQuickResolveButtonLabel', + { + defaultMessage: 'Try again', + } + ), + resolvedButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.resolvedButtonLabel', + { + defaultMessage: 'Resolved', + } + ), + quickResolveInProgressButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveInProgressButtonLabel', + { + defaultMessage: 'Resolution in progress…', + } + ), + quickResolveCalloutTitle: ( + + {i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveText', { + defaultMessage: 'Quick resolve', + })} + + ), + }} + /> + ), + quickResolveErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveErrorTitle', + { + defaultMessage: 'Error resolving issue', + } + ), + manualFixTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.manualFixTitle', + { + defaultMessage: 'How to fix', + } + ), +}; + +const getQuickResolveButtonLabel = (deprecationResolutionState?: DeprecationResolutionState) => { + if (deprecationResolutionState?.resolveDeprecationStatus === 'in_progress') { + return i18nTexts.quickResolveInProgressButtonLabel; + } + + if (deprecationResolutionState?.resolveDeprecationStatus === 'ok') { + return i18nTexts.resolvedButtonLabel; + } + + if (deprecationResolutionState?.resolveDeprecationError) { + return i18nTexts.retryQuickResolveButtonLabel; + } + + return i18nTexts.quickResolveButtonLabel; +}; + +export const DeprecationDetailsFlyout = ({ + deprecation, + closeFlyout, + resolveDeprecation, + deprecationResolutionState, +}: DeprecationDetailsFlyoutProps) => { + const { documentationUrl, message, correctiveActions, title } = deprecation; + const isCurrent = deprecationResolutionState?.id === deprecation.id; + const isResolved = isCurrent && deprecationResolutionState?.resolveDeprecationStatus === 'ok'; + + const onResolveDeprecation = useCallback(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_KIBANA_QUICK_RESOLVE_CLICK); + resolveDeprecation(deprecation); + }, [deprecation, resolveDeprecation]); + + return ( + <> + + + + +

+ {title} +

+
+
+ + {deprecationResolutionState?.resolveDeprecationStatus === 'fail' && ( + <> + + {deprecationResolutionState.resolveDeprecationError} + + + + )} + + +

{message}

+ {documentationUrl && ( +

+ +

+ )} +
+ + + + {/* Hide resolution steps if already resolved */} + {!isResolved && ( +
+ {correctiveActions.api && ( + <> + + + + + )} + + {correctiveActions.manualSteps.length > 0 && ( + <> + +

{i18nTexts.manualFixTitle}

+
+ + + {correctiveActions.manualSteps.length === 1 ? ( +

+ {correctiveActions.manualSteps[0]} +

+ ) : ( +
    + {correctiveActions.manualSteps.map((step, stepIndex) => ( +
  1. + {step} +
  2. + ))} +
+ )} +
+ + )} +
+ )} +
+ + + + + + {i18nTexts.closeButtonLabel} + + + + {/* Only show the "Quick resolve" button if deprecation supports it and deprecation is not yet resolved */} + {correctiveActions.api && !isResolved && ( + + + {getQuickResolveButtonLabel(deprecationResolutionState)} + + + )} + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx deleted file mode 100644 index 5bcc49590c55e..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_item.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { FunctionComponent } from 'react'; -import { - EuiAccordion, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiText, - EuiCallOut, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import type { DomainDeprecationDetails } from 'kibana/public'; -import { DeprecationHealth } from '../shared'; -import { LEVEL_MAP } from '../constants'; -import { StepsModalContent } from './steps_modal'; - -const i18nTexts = { - getDeprecationTitle: (domainId: string) => { - return i18n.translate('xpack.upgradeAssistant.deprecationGroupItemTitle', { - defaultMessage: "'{domainId}' is using a deprecated feature", - values: { - domainId, - }, - }); - }, - docLinkText: i18n.translate('xpack.upgradeAssistant.deprecationGroupItem.docLinkText', { - defaultMessage: 'View documentation', - }), - manualFixButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationGroupItem.fixButtonLabel', - { - defaultMessage: 'Show steps to fix', - } - ), - resolveButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationGroupItem.resolveButtonLabel', - { - defaultMessage: 'Quick resolve', - } - ), -}; - -export interface Props { - deprecation: DomainDeprecationDetails; - index: number; - forceExpand: boolean; - showStepsModal: (modalContent: StepsModalContent) => void; - showResolveModal: (deprecation: DomainDeprecationDetails) => void; -} - -export const KibanaDeprecationAccordion: FunctionComponent = ({ - deprecation, - forceExpand, - index, - showStepsModal, - showResolveModal, -}) => { - const { domainId, level, message, documentationUrl, correctiveActions } = deprecation; - - return ( - } - > - - - - {level === 'fetch_error' ? ( - - ) : ( - <> -

{message}

- - {(documentationUrl || correctiveActions?.manualSteps) && ( - - {correctiveActions?.api && ( - - showResolveModal(deprecation)} - > - {i18nTexts.resolveButtonLabel} - - - )} - - {correctiveActions?.manualSteps && ( - - - showStepsModal({ - domainId, - steps: correctiveActions.manualSteps!, - documentationUrl, - }) - } - > - {i18nTexts.manualFixButtonLabel} - - - )} - - {documentationUrl && ( - - - {i18nTexts.docLinkText} - - - )} - - )} - - )} -
-
-
-
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx deleted file mode 100644 index fb61efc373acf..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_list.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent, useState, useEffect } from 'react'; -import { groupBy } from 'lodash'; -import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; - -import type { DomainDeprecationDetails } from 'kibana/public'; - -import { LevelFilterOption } from '../types'; -import { SearchBar, DeprecationListBar, DeprecationPagination } from '../shared'; -import { DEPRECATIONS_PER_PAGE } from '../constants'; -import { KibanaDeprecationAccordion } from './deprecation_item'; -import { StepsModalContent } from './steps_modal'; -import { KibanaDeprecationErrors } from './kibana_deprecation_errors'; - -interface Props { - deprecations: DomainDeprecationDetails[]; - showStepsModal: (newStepsModalContent: StepsModalContent) => void; - showResolveModal: (deprecation: DomainDeprecationDetails) => void; - reloadDeprecations: () => Promise; - isLoading: boolean; -} - -const getFilteredDeprecations = ( - deprecations: DomainDeprecationDetails[], - level: LevelFilterOption, - search: string -) => { - return deprecations - .filter((deprecation) => { - return level === 'all' || deprecation.level === level; - }) - .filter((filteredDep) => { - if (search.length > 0) { - try { - // 'i' is used for case-insensitive matching - const searchReg = new RegExp(search, 'i'); - return searchReg.test(filteredDep.message); - } catch (e) { - // ignore any regexp errors - return true; - } - } - return true; - }); -}; - -export const KibanaDeprecationList: FunctionComponent = ({ - deprecations, - showStepsModal, - showResolveModal, - reloadDeprecations, - isLoading, -}) => { - const [currentFilter, setCurrentFilter] = useState('all'); - const [search, setSearch] = useState(''); - const [expandState, setExpandState] = useState({ - forceExpand: false, - expandNumber: 0, - }); - const [currentPage, setCurrentPage] = useState(0); - - const setExpandAll = (expandAll: boolean) => { - setExpandState({ forceExpand: expandAll, expandNumber: expandState.expandNumber + 1 }); - }; - - const levelGroups = groupBy(deprecations, 'level'); - const levelToDeprecationCountMap = Object.keys(levelGroups).reduce((counts, level) => { - counts[level] = levelGroups[level].length; - return counts; - }, {} as { [level: string]: number }); - - const filteredDeprecations = getFilteredDeprecations(deprecations, currentFilter, search); - - const deprecationsWithErrors = deprecations.filter((dep) => dep.level === 'fetch_error'); - - useEffect(() => { - const pageCount = Math.ceil(filteredDeprecations.length / DEPRECATIONS_PER_PAGE); - if (currentPage >= pageCount) { - setCurrentPage(0); - } - }, [filteredDeprecations, currentPage]); - - return ( - <> - - - {deprecationsWithErrors.length > 0 && ( - <> - - - - )} - - - - - - <> - {filteredDeprecations - .slice(currentPage * DEPRECATIONS_PER_PAGE, (currentPage + 1) * DEPRECATIONS_PER_PAGE) - .map((deprecation, index) => [ -
- - -
, - ])} - - {/* Only show pagination if we have more than DEPRECATIONS_PER_PAGE */} - {filteredDeprecations.length > DEPRECATIONS_PER_PAGE && ( - <> - - - - - )} - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts index 84d2b88757188..6a1375f57cd43 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { KibanaDeprecationsContent } from './kibana_deprecations'; +export { KibanaDeprecations } from './kibana_deprecations'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx deleted file mode 100644 index 79ada21941b56..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecation_errors.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui'; - -interface Props { - errorType: 'pluginError' | 'requestError'; -} - -const i18nTexts = { - pluginError: { - title: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorTitle', { - defaultMessage: 'Not all Kibana deprecations were retrieved successfully', - }), - description: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorDescription', - { - defaultMessage: 'Check the Kibana server logs for errors.', - } - ), - }, - loadingError: { - title: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorTitle', { - defaultMessage: 'Could not retrieve Kibana deprecations', - }), - description: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorDescription', - { - defaultMessage: 'Check the Kibana server logs for errors.', - } - ), - }, -}; - -export const KibanaDeprecationErrors: React.FunctionComponent = ({ errorType }) => { - if (errorType === 'pluginError') { - return ( - - {i18nTexts.pluginError.title}} - body={

{i18nTexts.pluginError.description}

} - /> -
- ); - } - - return ( - - {i18nTexts.loadingError.title}} - body={

{i18nTexts.loadingError.description}

} - /> -
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index 56d6e23d9d4f3..3b4cd5acafb95 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -5,29 +5,32 @@ * 2.0. */ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import uuid from 'uuid'; import { withRouter, RouteComponentProps } from 'react-router-dom'; - -import { EuiButtonEmpty, EuiPageContent, EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { EuiPageContent, EuiPageHeader, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import type { DomainDeprecationDetails } from 'kibana/public'; -import { SectionLoading } from '../../../shared_imports'; +import { SectionLoading, GlobalFlyout } from '../../../shared_imports'; import { useAppContext } from '../../app_context'; -import { NoDeprecationsPrompt } from '../shared'; -import { KibanaDeprecationList } from './deprecation_list'; -import { StepsModal, StepsModalContent } from './steps_modal'; -import { KibanaDeprecationErrors } from './kibana_deprecation_errors'; -import { ResolveDeprecationModal } from './resolve_deprecation_modal'; -import { LEVEL_MAP } from '../constants'; +import { uiMetricService, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; +import { DeprecationsPageLoadingError, NoDeprecationsPrompt, DeprecationCount } from '../shared'; +import { KibanaDeprecationsTable } from './kibana_deprecations_table'; +import { + DeprecationDetailsFlyout, + DeprecationDetailsFlyoutProps, +} from './deprecation_details_flyout'; + +const { useGlobalFlyout } = GlobalFlyout; const i18nTexts = { pageTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageTitle', { - defaultMessage: 'Kibana', + defaultMessage: 'Kibana deprecation issues', }), pageDescription: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageDescription', { - defaultMessage: - 'Review the issues listed here and make the necessary changes before upgrading. Critical issues must be resolved before you upgrade.', + defaultMessage: 'Resolve all critical issues before upgrading.', }), docLinkText: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.docLinkText', { defaultMessage: 'Documentation', @@ -36,43 +39,109 @@ const i18nTexts = { defaultMessage: 'Kibana', }), isLoading: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.loadingText', { - defaultMessage: 'Loading deprecations…', - }), - successMessage: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.successMessage', { - defaultMessage: 'Deprecation resolved', - }), - errorMessage: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.errorMessage', { - defaultMessage: 'Error resolving deprecation', + defaultMessage: 'Loading deprecation issues…', }), + kibanaDeprecationErrorTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.kibanaDeprecationErrorTitle', + { + defaultMessage: 'List of deprecation issues might be incomplete', + } + ), + getKibanaDeprecationErrorDescription: (pluginIds: string[]) => + i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.kibanaDeprecationErrorDescription', { + defaultMessage: + 'Failed to get deprecation issues for {pluginCount, plural, one {this plugin} other {these plugins}}: {pluginIds}. Check the Kibana server logs for more information.', + values: { + pluginCount: pluginIds.length, + pluginIds: pluginIds.join(', '), + }, + }), }; -const sortByLevelDesc = (a: DomainDeprecationDetails, b: DomainDeprecationDetails) => { - return -1 * (LEVEL_MAP[a.level] - LEVEL_MAP[b.level]); +export interface DeprecationResolutionState { + id: string; + resolveDeprecationStatus: 'ok' | 'fail' | 'in_progress'; + resolveDeprecationError?: string; +} + +export type KibanaDeprecationDetails = DomainDeprecationDetails & { + id: string; + filterType: DomainDeprecationDetails['deprecationType'] | 'uncategorized'; }; -export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponentProps) => { +const getDeprecationCountByLevel = (deprecations: KibanaDeprecationDetails[]) => { + const criticalDeprecations: KibanaDeprecationDetails[] = []; + const warningDeprecations: KibanaDeprecationDetails[] = []; + + deprecations.forEach((deprecation) => { + if (deprecation.level === 'critical') { + criticalDeprecations.push(deprecation); + return; + } + warningDeprecations.push(deprecation); + }); + + return { + criticalDeprecations: criticalDeprecations.length, + warningDeprecations: warningDeprecations.length, + }; +}; + +export const KibanaDeprecations = withRouter(({ history }: RouteComponentProps) => { const [kibanaDeprecations, setKibanaDeprecations] = useState< - DomainDeprecationDetails[] | undefined + KibanaDeprecationDetails[] | undefined >(undefined); + const [kibanaDeprecationErrors, setKibanaDeprecationErrors] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); - const [stepsModalContent, setStepsModalContent] = useState( + const [flyoutContent, setFlyoutContent] = useState( undefined ); - const [resolveModalContent, setResolveModalContent] = useState< - undefined | DomainDeprecationDetails + const [deprecationResolutionState, setDeprecationResolutionState] = useState< + DeprecationResolutionState | undefined >(undefined); - const [isResolvingDeprecation, setIsResolvingDeprecation] = useState(false); - const { deprecations, breadcrumbs, docLinks, api, notifications } = useAppContext(); + const { + services: { + core: { deprecations }, + breadcrumbs, + }, + } = useAppContext(); + + const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = + useGlobalFlyout(); const getAllDeprecations = useCallback(async () => { setIsLoading(true); try { - const response = await deprecations.getAllDeprecations(); - const sortedDeprecations = response.sort(sortByLevelDesc); - setKibanaDeprecations(sortedDeprecations); + const allDeprecations = await deprecations.getAllDeprecations(); + + const filteredDeprecations: KibanaDeprecationDetails[] = []; + const deprecationErrors: string[] = []; + + allDeprecations.forEach((deprecation) => { + // Keep track of any plugin deprecations that failed to fetch to show warning in UI + if (deprecation.level === 'fetch_error') { + // It's possible that a plugin registered more than one deprecation that could fail + // We only want to keep track of the unique plugin failures + const pluginErrorExists = deprecationErrors.includes(deprecation.domainId); + if (pluginErrorExists === false) { + deprecationErrors.push(deprecation.domainId); + } + return; + } + + // Only show deprecations in the table that fetched successfully + filteredDeprecations.push({ + ...deprecation, + id: uuid.v4(), // Associate an unique ID with each deprecation to track resolution state + filterType: deprecation.deprecationType ?? 'uncategorized', // deprecationType is currently optional, in order to correctly handle sort/filter, we default any undefined types to "uncategorized" + }); + }); + + setKibanaDeprecations(filteredDeprecations); + setKibanaDeprecationErrors(deprecationErrors); } catch (e) { setError(e); } @@ -80,45 +149,72 @@ export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponent setIsLoading(false); }, [deprecations]); - const toggleStepsModal = (newStepsModalContent?: StepsModalContent) => { - setStepsModalContent(newStepsModalContent); - }; + const deprecationsCountByLevel: { + warningDeprecations: number; + criticalDeprecations: number; + } = useMemo(() => getDeprecationCountByLevel(kibanaDeprecations || []), [kibanaDeprecations]); - const toggleResolveModal = (newResolveModalContent?: DomainDeprecationDetails) => { - setResolveModalContent(newResolveModalContent); + const toggleFlyout = (newFlyoutContent?: KibanaDeprecationDetails) => { + setFlyoutContent(newFlyoutContent); }; - const resolveDeprecation = async (deprecationDetails: DomainDeprecationDetails) => { - setIsResolvingDeprecation(true); + const closeFlyout = useCallback(() => { + toggleFlyout(); + removeContentFromGlobalFlyout('deprecationDetails'); + }, [removeContentFromGlobalFlyout]); - const response = await deprecations.resolveDeprecation(deprecationDetails); + const resolveDeprecation = useCallback( + async (deprecationDetails: KibanaDeprecationDetails) => { + setDeprecationResolutionState({ + id: deprecationDetails.id, + resolveDeprecationStatus: 'in_progress', + }); - setIsResolvingDeprecation(false); - toggleResolveModal(); + const response = await deprecations.resolveDeprecation(deprecationDetails); - // Handle error case - if (response.status === 'fail') { - notifications.toasts.addError(new Error(response.reason), { - title: i18nTexts.errorMessage, + setDeprecationResolutionState({ + id: deprecationDetails.id, + resolveDeprecationStatus: response.status, + resolveDeprecationError: response.status === 'fail' ? response.reason : undefined, }); - return; - } - - notifications.toasts.addSuccess(i18nTexts.successMessage); - // Refetch deprecations - getAllDeprecations(); - }; + closeFlyout(); + }, + [closeFlyout, deprecations] + ); useEffect(() => { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - kibana: true, + if (flyoutContent) { + addContentToGlobalFlyout({ + id: 'deprecationDetails', + Component: DeprecationDetailsFlyout, + props: { + deprecation: flyoutContent, + closeFlyout, + resolveDeprecation, + deprecationResolutionState: + deprecationResolutionState && flyoutContent.id === deprecationResolutionState.id + ? deprecationResolutionState + : undefined, + }, + flyoutProps: { + onClose: closeFlyout, + 'data-test-subj': 'kibanaDeprecationDetails', + 'aria-labelledby': 'kibanaDeprecationDetailsFlyoutTitle', + }, }); } + }, [ + addContentToGlobalFlyout, + closeFlyout, + deprecationResolutionState, + flyoutContent, + resolveDeprecation, + ]); - sendTelemetryData(); - }, [api]); + useEffect(() => { + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD); + }, []); useEffect(() => { breadcrumbs.setBreadcrumbs('kibanaDeprecations'); @@ -128,69 +224,65 @@ export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponent getAllDeprecations(); }, [deprecations, getAllDeprecations]); - if (kibanaDeprecations && kibanaDeprecations.length === 0) { + if (error) { + return ; + } + + if (isLoading) { return ( - history.push('/overview')} - /> + {i18nTexts.isLoading} ); } - if (isLoading) { + if (kibanaDeprecations?.length === 0) { return ( - {i18nTexts.isLoading} + history.push('/overview')} + /> ); - } else if (kibanaDeprecations?.length) { - return ( -
- - {i18nTexts.docLinkText} - , - ]} + } + + return ( +
+ + + - + - + {kibanaDeprecationErrors.length > 0 && ( + <> + +

{i18nTexts.getKibanaDeprecationErrorDescription(kibanaDeprecationErrors)}

+
- {stepsModalContent && ( - toggleStepsModal()} modalContent={stepsModalContent} /> - )} - - {resolveModalContent && ( - toggleResolveModal()} - resolveDeprecation={resolveDeprecation} - isResolvingDeprecation={isResolvingDeprecation} - deprecation={resolveModalContent} - /> - )} -
- ); - } else if (error) { - return ; - } + + + )} - return null; + +
+ ); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx new file mode 100644 index 0000000000000..6a757d0cb2b0b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink, Search } from '@elastic/eui'; + +import { PAGINATION_CONFIG } from '../constants'; +import type { DeprecationResolutionState, KibanaDeprecationDetails } from './kibana_deprecations'; +import { ResolutionTableCell } from './resolution_table_cell'; +import { DeprecationBadge } from '../shared'; + +const i18nTexts = { + refreshButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.refreshButtonLabel', + { + defaultMessage: 'Refresh', + } + ), + statusColumnTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.statusColumnTitle', + { + defaultMessage: 'Status', + } + ), + issueColumnTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.issueColumnTitle', + { + defaultMessage: 'Issue', + } + ), + typeColumnTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.typeColumnTitle', + { + defaultMessage: 'Type', + } + ), + resolutionColumnTitle: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.resolutionColumnTitle', + { + defaultMessage: 'Resolution', + } + ), + configDeprecationTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.configDeprecationTypeCellLabel', + { + defaultMessage: 'Config', + } + ), + featureDeprecationTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.featureDeprecationTypeCellLabel', + { + defaultMessage: 'Feature', + } + ), + unknownDeprecationTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.unknownDeprecationTypeCellLabel', + { + defaultMessage: 'Uncategorized', + } + ), + typeFilterLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.typeFilterLabel', + { + defaultMessage: 'Type', + } + ), + criticalFilterLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.criticalFilterLabel', + { + defaultMessage: 'Critical', + } + ), + searchPlaceholderLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.searchPlaceholderLabel', + { + defaultMessage: 'Filter', + } + ), +}; + +interface Props { + deprecations?: KibanaDeprecationDetails[]; + reload: () => void; + toggleFlyout: (newFlyoutContent?: KibanaDeprecationDetails) => void; + deprecationResolutionState?: DeprecationResolutionState; +} + +export const KibanaDeprecationsTable: React.FunctionComponent = ({ + deprecations, + reload, + toggleFlyout, + deprecationResolutionState, +}) => { + const columns: Array> = [ + { + field: 'level', + name: i18nTexts.statusColumnTitle, + width: '5%', + truncateText: true, + sortable: true, + render: (level: KibanaDeprecationDetails['level']) => { + return ; + }, + }, + { + field: 'title', + width: '40%', + name: i18nTexts.issueColumnTitle, + truncateText: true, + sortable: true, + render: (title: KibanaDeprecationDetails['title'], deprecation: KibanaDeprecationDetails) => { + return ( + toggleFlyout(deprecation)} + data-test-subj="deprecationDetailsLink" + > + {title} + + ); + }, + }, + { + field: 'filterType', + name: i18nTexts.typeColumnTitle, + width: '20%', + truncateText: true, + sortable: true, + render: (filterType: KibanaDeprecationDetails['filterType']) => { + switch (filterType) { + case 'config': + return i18nTexts.configDeprecationTypeCellLabel; + case 'feature': + return i18nTexts.featureDeprecationTypeCellLabel; + case 'uncategorized': + default: + return i18nTexts.unknownDeprecationTypeCellLabel; + } + }, + }, + { + field: 'correctiveActions', + name: i18nTexts.resolutionColumnTitle, + width: '30%', + truncateText: true, + sortable: true, + render: ( + correctiveActions: KibanaDeprecationDetails['correctiveActions'], + deprecation: KibanaDeprecationDetails + ) => { + return ( + + ); + }, + }, + ]; + + const sorting = { + sort: { + field: 'level', + direction: 'asc', + }, + } as const; + + const searchConfig: Search = { + filters: [ + { + type: 'field_value_toggle', + name: i18nTexts.criticalFilterLabel, + field: 'level', + value: 'critical', + }, + { + type: 'field_value_selection', + field: 'filterType', + name: i18nTexts.typeFilterLabel, + multiSelect: false, + options: [ + { + value: 'config', + name: i18nTexts.configDeprecationTypeCellLabel, + }, + { + value: 'feature', + name: i18nTexts.featureDeprecationTypeCellLabel, + }, + { + value: 'uncategorized', + name: i18nTexts.unknownDeprecationTypeCellLabel, + }, + ], + }, + ], + box: { + incremental: true, + placeholder: i18nTexts.searchPlaceholderLabel, + }, + toolsRight: [ + + {i18nTexts.refreshButtonLabel} + , + ], + }; + + return ( + ({ + 'data-test-subj': 'row', + })} + cellProps={(deprecation, field) => ({ + 'data-test-subj': `${((field?.name as string) || 'table').toLowerCase()}Cell`, + })} + data-test-subj="kibanaDeprecationsTable" + tableLayout="auto" + /> + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx new file mode 100644 index 0000000000000..daf276b7ed3f8 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiFlexItem, + EuiText, + EuiFlexGroup, + EuiIcon, + EuiLoadingSpinner, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import type { DeprecationResolutionState } from './kibana_deprecations'; + +const i18nTexts = { + manualCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.manualCellLabel', + { + defaultMessage: 'Manual', + } + ), + manualCellTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.manualCellTooltipLabel', + { + defaultMessage: 'This issue needs to be resolved manually.', + } + ), + automatedCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automatedCellLabel', + { + defaultMessage: 'Automated', + } + ), + automationInProgressCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automationInProgressCellLabel', + { + defaultMessage: 'Resolution in progress…', + } + ), + automationCompleteCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automationCompleteCellLabel', + { + defaultMessage: 'Resolved', + } + ), + automationFailedCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automationFailedCellLabel', + { + defaultMessage: 'Resolution failed', + } + ), + automatedCellTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.automatedCellTooltipLabel', + { + defaultMessage: 'This issue can be resolved automatically.', + } + ), +}; + +interface Props { + deprecationId: string; + isAutomated: boolean; + deprecationResolutionState?: DeprecationResolutionState; +} + +export const ResolutionTableCell: React.FunctionComponent = ({ + deprecationId, + isAutomated, + deprecationResolutionState, +}) => { + if (isAutomated) { + if (deprecationResolutionState?.id === deprecationId) { + const { resolveDeprecationStatus } = deprecationResolutionState; + + switch (resolveDeprecationStatus) { + case 'in_progress': + return ( + + + + + + {i18nTexts.automationInProgressCellLabel} + + + ); + case 'fail': + return ( + + + + + + {i18nTexts.automationFailedCellLabel} + + + ); + case 'ok': + default: + return ( + + + + + + {i18nTexts.automationCompleteCellLabel} + + + ); + } + } + + return ( + + + + + + + {i18nTexts.automatedCellLabel} + + + + ); + } + + return ( + + + {i18nTexts.manualCellLabel} + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx deleted file mode 100644 index f94512fac5630..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolve_deprecation_modal.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { EuiConfirmModal } from '@elastic/eui'; -import type { DomainDeprecationDetails } from 'kibana/public'; - -interface Props { - closeModal: () => void; - deprecation: DomainDeprecationDetails; - isResolvingDeprecation: boolean; - resolveDeprecation: (deprecationDetails: DomainDeprecationDetails) => Promise; -} - -const i18nTexts = { - getModalTitle: (domainId: string) => - i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.modalTitle', - { - defaultMessage: "Resolve deprecation in '{domainId}'?", - values: { - domainId, - }, - } - ), - cancelButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } - ), - resolveButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.resolveButtonLabel', - { - defaultMessage: 'Resolve', - } - ), -}; - -export const ResolveDeprecationModal: FunctionComponent = ({ - closeModal, - deprecation, - isResolvingDeprecation, - resolveDeprecation, -}) => { - return ( - resolveDeprecation(deprecation)} - cancelButtonText={i18nTexts.cancelButtonLabel} - confirmButtonText={i18nTexts.resolveButtonLabel} - defaultFocusedButton="confirm" - isLoading={isResolvingDeprecation} - /> - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx deleted file mode 100644 index 98027d4f46aac..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/steps_modal.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { - EuiText, - EuiSteps, - EuiButton, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiTitle, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; - -export interface StepsModalContent { - domainId: string; - steps: string[]; - documentationUrl?: string; -} - -interface Props { - closeModal: () => void; - modalContent: StepsModalContent; -} - -const i18nTexts = { - getModalTitle: (domainId: string) => - i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalTitle', { - defaultMessage: "Resolve deprecation in '{domainId}'", - values: { - domainId, - }, - }), - getStepTitle: (step: number) => - i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.stepsModal.stepTitle', { - defaultMessage: 'Step {step}', - values: { - step, - }, - }), - docLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.docLinkLabel', - { - defaultMessage: 'View documentation', - } - ), - closeButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.closeButtonLabel', - { - defaultMessage: 'Close', - } - ), -}; - -export const StepsModal: FunctionComponent = ({ closeModal, modalContent }) => { - const { domainId, steps, documentationUrl } = modalContent; - - return ( - - - - -

{i18nTexts.getModalTitle(domainId)}

-
-
-
- - - { - return { - title: i18nTexts.getStepTitle(index + 1), - children: ( - -

{step}

-
- ), - }; - })} - /> -
- - - - {documentationUrl && ( - - - {i18nTexts.docLinkLabel} - - - )} - - - - {i18nTexts.closeButtonLabel} - - - - -
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss deleted file mode 100644 index cbcfbff3bab68..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'review_logs_step/index'; -@import 'fix_deprecation_logs_step/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx new file mode 100644 index 0000000000000..46b11aee15b33 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import type { CloudSetup } from '../../../../../../cloud/public'; +import { OnPremBackup } from './on_prem_backup'; +import { CloudBackup } from './cloud_backup'; +import type { OverviewStepProps } from '../../types'; + +const title = i18n.translate('xpack.upgradeAssistant.overview.backupStepTitle', { + defaultMessage: 'Back up your data', +}); + +interface Props extends OverviewStepProps { + cloud?: CloudSetup; +} + +export const getBackupStep = ({ cloud, isComplete, setIsComplete }: Props): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + if (cloud?.isCloudEnabled) { + return { + status, + title, + 'data-test-subj': `backupStep-${status}`, + children: ( + + ), + }; + } + + return { + title, + 'data-test-subj': 'backupStep-incomplete', + status: 'incomplete', + children: , + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx new file mode 100644 index 0000000000000..4ab860a0bf6a7 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import moment from 'moment-timezone'; +import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { + EuiLoadingContent, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiButton, + EuiSpacer, + EuiCallOut, +} from '@elastic/eui'; + +import { useAppContext } from '../../../app_context'; +import { uiMetricService, UIM_BACKUP_DATA_CLOUD_CLICK } from '../../../lib/ui_metric'; + +interface Props { + cloudSnapshotsUrl: string; + setIsComplete: (isComplete: boolean) => void; +} + +export const CloudBackup: React.FunctionComponent = ({ + cloudSnapshotsUrl, + setIsComplete, +}) => { + const { + services: { api }, + } = useAppContext(); + + const { isInitialRequest, isLoading, error, data, resendRequest } = + api.useLoadCloudBackupStatus(); + + // Tell overview whether the step is complete or not. + useEffect(() => { + // Loading shouldn't invalidate the previous state. + if (!isLoading) { + // An error should invalidate the previous state. + setIsComplete((!error && data?.isBackedUp) ?? false); + } + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading, data]); + + if (isInitialRequest && isLoading) { + return ; + } + + if (error) { + return ( + +

+ {error.statusCode} - {error.message} +

+ + {i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.retryButton', { + defaultMessage: 'Try again', + })} + +
+ ); + } + + const lastBackupTime = moment(data!.lastBackupTime).toISOString(); + + const statusMessage = data!.isBackedUp ? ( + + + + + + + +

+ + {' '} + + + ), + }} + /> +

+
+
+
+ ) : ( + + + + + + + +

+ {i18n.translate('xpack.upgradeAssistant.overview.cloudBackup.noSnapshotMessage', { + defaultMessage: `Your data isn't backed up.`, + })} +

+
+
+
+ ); + + return ( + <> + {statusMessage} + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_BACKUP_DATA_CLOUD_CLICK); + }} + data-test-subj="cloudSnapshotsLink" + target="_blank" + iconType="popout" + iconSide="right" + > + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/index.ts similarity index 84% rename from x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/index.ts index 31ad78cf572fe..8daac9645fa12 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { SearchBar } from './search_bar'; +export { getBackupStep } from './backup_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx new file mode 100644 index 0000000000000..e512eb5a301dc --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/on_prem_backup.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; + +import { useAppContext } from '../../../app_context'; +import { uiMetricService, UIM_BACKUP_DATA_ON_PREM_CLICK } from '../../../lib/ui_metric'; + +const SnapshotRestoreAppLink: React.FunctionComponent = () => { + const { + plugins: { share }, + } = useAppContext(); + + const snapshotRestoreUrl = share.url.locators + .get('SNAPSHOT_RESTORE_LOCATOR') + ?.useUrl({ page: 'snapshots' }); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_BACKUP_DATA_ON_PREM_CLICK); + }} + data-test-subj="snapshotRestoreLink" + > + + + ); +}; + +export const OnPremBackup: React.FunctionComponent = () => { + return ( + <> + +

+ {i18n.translate('xpack.upgradeAssistant.overview.backupStepDescription', { + defaultMessage: 'Make sure you have a current snapshot before making any changes.', + })} +

+
+ + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/_index.scss deleted file mode 100644 index 2299c08a4ac31..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'deprecation_logging_toggle/deprecation_logging_toggle'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/external_links.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/external_links.tsx deleted file mode 100644 index 0cd5ad5bfdb2f..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/external_links.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent, useState, useEffect } from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiText } from '@elastic/eui'; - -import { useAppContext } from '../../../app_context'; -import { useKibana, DataPublicPluginStart } from '../../../../shared_imports'; -import { - DEPRECATION_LOGS_INDEX_PATTERN, - DEPRECATION_LOGS_SOURCE_ID, -} from '../../../../../common/constants'; - -const getDeprecationIndexPatternId = async (dataService: DataPublicPluginStart) => { - const { indexPatterns: indexPatternService } = dataService; - - const results = await indexPatternService.find(DEPRECATION_LOGS_INDEX_PATTERN); - // Since the find might return also results with wildcard matchers we need to find the - // index pattern that has an exact match with our title. - const deprecationIndexPattern = results.find( - (result) => result.title === DEPRECATION_LOGS_INDEX_PATTERN - ); - - if (deprecationIndexPattern) { - return deprecationIndexPattern.id; - } else { - const newIndexPattern = await indexPatternService.createAndSave({ - title: DEPRECATION_LOGS_INDEX_PATTERN, - allowNoIndex: true, - }); - return newIndexPattern.id; - } -}; - -const DiscoverAppLink: FunctionComponent = () => { - const { getUrlForApp } = useAppContext(); - const { data: dataService, discover: discoverService } = useKibana().services; - - const [discoveryUrl, setDiscoveryUrl] = useState(); - - useEffect(() => { - const getDiscoveryUrl = async () => { - const indexPatternId = await getDeprecationIndexPatternId(dataService); - const appLocation = await discoverService?.locator?.getLocation({ indexPatternId }); - - const result = getUrlForApp(appLocation?.app as string, { - path: appLocation?.path, - }); - setDiscoveryUrl(result); - }; - - getDiscoveryUrl(); - }, [dataService, discoverService, getUrlForApp]); - - return ( - - - - ); -}; - -const ObservabilityAppLink: FunctionComponent = () => { - const { http } = useAppContext(); - const logStreamUrl = http?.basePath?.prepend( - `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}` - ); - - return ( - - - - ); -}; - -export const ExternalLinks: FunctionComponent = () => { - return ( - - - - -

- -

-
- - -
-
- - - -

- -

-
- - -
-
-
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.tsx deleted file mode 100644 index a2f1feae4979d..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/fix_deprecation_logs_step.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { EuiText, EuiSpacer, EuiPanel, EuiCallOut } from '@elastic/eui'; -import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; - -import { ExternalLinks } from './external_links'; -import { useDeprecationLogging } from './use_deprecation_logging'; -import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; - -const i18nTexts = { - identifyStepTitle: i18n.translate('xpack.upgradeAssistant.overview.identifyStepTitle', { - defaultMessage: 'Identify deprecated API use and update your applications', - }), - toggleTitle: i18n.translate('xpack.upgradeAssistant.overview.toggleTitle', { - defaultMessage: 'Log Elasticsearch deprecation warnings', - }), - analyzeTitle: i18n.translate('xpack.upgradeAssistant.overview.analyzeTitle', { - defaultMessage: 'Analyze deprecation logs', - }), - onlyLogWritingEnabledTitle: i18n.translate( - 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningTitle', - { - defaultMessage: 'Your logs are being written to the logs directory', - } - ), - onlyLogWritingEnabledBody: i18n.translate( - 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningBody', - { - defaultMessage: - 'Go to your logs directory to view the deprecation logs or enable log collecting to see them in the UI.', - } - ), -}; - -const DeprecationLogsPreview: FunctionComponent = () => { - const state = useDeprecationLogging(); - - return ( - <> - -

{i18nTexts.toggleTitle}

-
- - - - - - {state.onlyDeprecationLogWritingEnabled && ( - <> - - -

{i18nTexts.onlyLogWritingEnabledBody}

-
- - )} - - {state.isDeprecationLogIndexingEnabled && ( - <> - - -

{i18nTexts.analyzeTitle}

-
- - - - )} - - ); -}; - -export const getFixDeprecationLogsStep = (): EuiStepProps => { - return { - title: i18nTexts.identifyStepTitle, - status: 'incomplete', - children: , - }; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/_deprecation_issues_panel.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/_deprecation_issues_panel.scss new file mode 100644 index 0000000000000..37079275b1859 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/_deprecation_issues_panel.scss @@ -0,0 +1,24 @@ +/** + * Push success state to the bottom + * of the card, so it aligns with , + * which is inside EuiStat. + */ +.upgDeprecationIssuesPanel .euiCard__content { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +/** + * Ensure the stat is a consistent height, even when it contains + * , which is shorter than the + * standard number value. We also push it to the bottom of the its + * container, to base-align it with the number value. + */ +.upgDeprecationIssuesPanel__stat { + height: 60px; // Derived from font measurements, not sizing vars + justify-content: space-between; + flex-grow: 1; + flex-direction: column; + display: flex; +} \ No newline at end of file diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/deprecation_issues_panel.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/deprecation_issues_panel.tsx new file mode 100644 index 0000000000000..8c42e71c0ef2b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/deprecation_issues_panel.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiCard, EuiStat, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { reactRouterNavigate } from '../../../../../shared_imports'; +import { DeprecationSource } from '../../../../../../common/types'; +import { getDeprecationsUpperLimit } from '../../../../lib/utils'; +import { LoadingIssuesError } from './loading_issues_error'; +import { NoDeprecationIssues } from './no_deprecation_issues'; + +import './_deprecation_issues_panel.scss'; + +const i18nTexts = { + warningDeprecationsTitle: i18n.translate( + 'xpack.upgradeAssistant.deprecationStats.warningDeprecationsTitle', + { + defaultMessage: 'Warning', + } + ), + criticalDeprecationsTitle: i18n.translate( + 'xpack.upgradeAssistant.deprecationStats.criticalDeprecationsTitle', + { + defaultMessage: 'Critical', + } + ), +}; + +interface Props { + 'data-test-subj': string; + deprecationSource: DeprecationSource; + linkUrl: string; + criticalDeprecationsCount: number; + warningDeprecationsCount: number; + isLoading: boolean; + errorMessage?: JSX.Element | string | null; + setIsFixed: (isFixed: boolean) => void; +} + +export const DeprecationIssuesPanel = (props: Props) => { + const { + deprecationSource, + linkUrl, + criticalDeprecationsCount, + warningDeprecationsCount, + isLoading, + errorMessage, + setIsFixed, + } = props; + const history = useHistory(); + + const hasError = !!errorMessage; + const hasCriticalIssues = criticalDeprecationsCount > 0; + const hasWarningIssues = warningDeprecationsCount > 0; + const hasNoIssues = !isLoading && !hasError && !hasWarningIssues && !hasCriticalIssues; + + useEffect(() => { + if (!isLoading && !errorMessage) { + setIsFixed(criticalDeprecationsCount === 0); + } + }, [setIsFixed, criticalDeprecationsCount, isLoading, errorMessage]); + + return ( + + + + {hasError ? ( + {errorMessage} + ) : hasNoIssues ? ( + + ) : ( + + + + ) + } + titleElement="span" + description={i18nTexts.criticalDeprecationsTitle} + titleColor="danger" + isLoading={isLoading} + /> + + + + + ) + } + titleElement="span" + description={i18nTexts.warningDeprecationsTitle} + isLoading={isLoading} + /> + + + )} + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.tsx new file mode 100644 index 0000000000000..b4258ababc92e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/es_deprecation_issues_panel.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; + +import { useAppContext } from '../../../../app_context'; +import { getEsDeprecationError } from '../../../../lib/get_es_deprecation_error'; +import { DeprecationIssuesPanel } from './deprecation_issues_panel'; + +interface Props { + setIsFixed: (isFixed: boolean) => void; +} + +export const EsDeprecationIssuesPanel: FunctionComponent = ({ setIsFixed }) => { + const { + services: { api }, + } = useAppContext(); + + const { data: esDeprecations, isLoading, error } = api.useLoadEsDeprecations(); + + const criticalDeprecationsCount = + esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical)?.length ?? 0; + + const warningDeprecationsCount = + esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) + ?.length ?? 0; + + const errorMessage = error && getEsDeprecationError(error).message; + + return ( + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.ts new file mode 100644 index 0000000000000..a2a3219002719 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { EsDeprecationIssuesPanel } from './es_deprecation_issues_panel'; +export { KibanaDeprecationIssuesPanel } from './kibana_deprecation_issues_panel'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx new file mode 100644 index 0000000000000..b0aa7b592e3a1 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/kibana_deprecation_issues_panel.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { DomainDeprecationDetails } from 'kibana/public'; + +import { useAppContext } from '../../../../app_context'; +import { DeprecationIssuesPanel } from './deprecation_issues_panel'; + +interface Props { + setIsFixed: (isFixed: boolean) => void; +} + +export const KibanaDeprecationIssuesPanel: FunctionComponent = ({ setIsFixed }) => { + const { + services: { + core: { deprecations }, + }, + } = useAppContext(); + + const [kibanaDeprecations, setKibanaDeprecations] = useState< + DomainDeprecationDetails[] | undefined + >(undefined); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + useEffect(() => { + async function getAllDeprecations() { + setIsLoading(true); + + try { + const response = await deprecations.getAllDeprecations(); + setKibanaDeprecations(response); + } catch (e) { + setError(e); + } + + setIsLoading(false); + } + + getAllDeprecations(); + }, [deprecations]); + + const criticalDeprecationsCount = + kibanaDeprecations?.filter((deprecation) => deprecation.level === 'critical')?.length ?? 0; + + const warningDeprecationsCount = + kibanaDeprecations?.filter((deprecation) => deprecation.level === 'warning')?.length ?? 0; + + const errorMessage = + error && + i18n.translate('xpack.upgradeAssistant.deprecationStats.loadingErrorMessage', { + defaultMessage: 'Could not retrieve Kibana deprecation issues.', + }); + + return ( + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/loading_issues_error.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/loading_issues_error.tsx new file mode 100644 index 0000000000000..cdd406dc8622b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/loading_issues_error.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; + +export const LoadingIssuesError: FunctionComponent = ({ children }) => ( + + + + + + + {children} + + +); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/no_deprecation_issues.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/no_deprecation_issues.tsx new file mode 100644 index 0000000000000..168a682ab6d33 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/components/no_deprecation_issues.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const i18nTexts = { + noPartialDeprecationIssuesText: i18n.translate( + 'xpack.upgradeAssistant.noPartialDeprecationsMessage', + { + defaultMessage: 'None', + } + ), + noDeprecationIssuesText: i18n.translate('xpack.upgradeAssistant.noDeprecationsMessage', { + defaultMessage: 'No issues', + }), +}; + +interface Props { + isPartial?: boolean; + 'data-test-subj'?: string; +} + +export const NoDeprecationIssues: FunctionComponent = (props) => { + const { isPartial = false } = props; + + return ( + + + + + + + + {isPartial ? i18nTexts.noPartialDeprecationIssuesText : i18nTexts.noDeprecationIssuesText} + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx new file mode 100644 index 0000000000000..61d25404b2aee --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx @@ -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 React, { FunctionComponent, useState, useEffect } from 'react'; + +import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import type { OverviewStepProps } from '../../types'; +import { EsDeprecationIssuesPanel, KibanaDeprecationIssuesPanel } from './components'; + +const i18nTexts = { + reviewStepTitle: i18n.translate('xpack.upgradeAssistant.overview.fixIssuesStepTitle', { + defaultMessage: 'Review deprecated settings and resolve issues', + }), +}; + +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; +} + +const FixIssuesStep: FunctionComponent = ({ setIsComplete }) => { + // We consider ES and Kibana issues to be fixed when there are 0 critical issues. + const [isEsFixed, setIsEsFixed] = useState(false); + const [isKibanaFixed, setIsKibanaFixed] = useState(false); + + useEffect(() => { + setIsComplete(isEsFixed && isKibanaFixed); + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEsFixed, isKibanaFixed]); + + return ( + + + + + + + + + + ); +}; + +export const getFixIssuesStep = ({ + isComplete, + setIsComplete, +}: OverviewStepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + return { + title: i18nTexts.reviewStepTitle, + status, + 'data-test-subj': `fixIssuesStep-${status}`, + children: ( + <> + +

+ +

+
+ + + + + + ), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/index.ts similarity index 82% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/index.ts index a2684505eb9c6..dde6996edfc74 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { NoDeprecations } from './no_deprecations'; +export { getFixIssuesStep } from './fix_issues_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/_deprecation_logging_toggle.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/_deprecation_logging_toggle.scss similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/_deprecation_logging_toggle.scss rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/_deprecation_logging_toggle.scss diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx similarity index 89% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx index 42b9f073a52f1..cddf5101a4b43 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/deprecation_logging_toggle.tsx @@ -20,9 +20,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ResponseError } from '../../../../lib/api'; +import { ResponseError } from '../../../../../../common/types'; import { DeprecationLoggingPreviewProps } from '../../../types'; +import './_deprecation_logging_toggle.scss'; + const i18nTexts = { fetchErrorMessage: i18n.translate( 'xpack.upgradeAssistant.overview.deprecationLogs.fetchErrorMessage', @@ -46,10 +48,10 @@ const i18nTexts = { defaultMessage: 'Error', }), buttonLabel: i18n.translate('xpack.upgradeAssistant.overview.deprecationLogs.buttonLabel', { - defaultMessage: 'Enable deprecation logging and indexing', + defaultMessage: 'Enable deprecation log collection', }), loadingLogsLabel: i18n.translate('xpack.upgradeAssistant.overview.loadingLogsLabel', { - defaultMessage: 'Loading log collection state…', + defaultMessage: 'Loading deprecation log collection state…', }), }; @@ -77,7 +79,18 @@ const ErrorDetailsLink = ({ error }: { error: ResponseError }) => { ); }; -export const DeprecationLoggingToggle: FunctionComponent = ({ +type Props = Pick< + DeprecationLoggingPreviewProps, + | 'isDeprecationLogIndexingEnabled' + | 'isLoading' + | 'isUpdating' + | 'fetchError' + | 'updateError' + | 'resendRequest' + | 'toggleLogging' +>; + +export const DeprecationLoggingToggle: FunctionComponent = ({ isDeprecationLogIndexingEnabled, isLoading, isUpdating, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/index.ts similarity index 100% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/deprecation_logging_toggle/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecation_logging_toggle/index.ts diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx new file mode 100644 index 0000000000000..6ce1fec32d66c --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx @@ -0,0 +1,149 @@ +/* + * 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, { FunctionComponent, useEffect, useState } from 'react'; +import moment from 'moment-timezone'; +import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut, EuiButton, EuiLoadingContent } from '@elastic/eui'; + +import { useAppContext } from '../../../../app_context'; +import { uiMetricService, UIM_RESET_LOGS_COUNTER_CLICK } from '../../../../lib/ui_metric'; + +const i18nTexts = { + calloutTitle: (warningsCount: number, previousCheck: string) => ( + + {' '} + + + ), + }} + /> + ), + calloutBody: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.calloutBody', { + defaultMessage: `After making changes, reset the counter and continue monitoring to verify you're no longer using deprecated features.`, + }), + loadingError: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.loadingError', { + defaultMessage: 'An error occurred while retrieving the count of deprecation logs', + }), + retryButton: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.retryButton', { + defaultMessage: 'Try again', + }), + resetCounterButton: i18n.translate( + 'xpack.upgradeAssistant.overview.verifyChanges.resetCounterButton', + { + defaultMessage: 'Reset counter', + } + ), + errorToastTitle: i18n.translate('xpack.upgradeAssistant.overview.verifyChanges.errorToastTitle', { + defaultMessage: 'Could not delete deprecation logs cache', + }), +}; + +interface Props { + checkpoint: string; + setCheckpoint: (value: string) => void; + setHasNoDeprecationLogs: (hasNoLogs: boolean) => void; +} + +export const DeprecationsCountCheckpoint: FunctionComponent = ({ + checkpoint, + setCheckpoint, + setHasNoDeprecationLogs, +}) => { + const [isDeletingCache, setIsDeletingCache] = useState(false); + const { + services: { + api, + core: { notifications }, + }, + } = useAppContext(); + const { data, error, isLoading, resendRequest, isInitialRequest } = + api.getDeprecationLogsCount(checkpoint); + + const logsCount = data?.count || 0; + const hasLogs = logsCount > 0; + const calloutTint = hasLogs ? 'warning' : 'success'; + const calloutIcon = hasLogs ? 'alert' : 'check'; + const calloutTestId = hasLogs ? 'hasWarningsCallout' : 'noWarningsCallout'; + + const onResetClick = async () => { + setIsDeletingCache(true); + const { error: deleteLogsCacheError } = await api.deleteDeprecationLogsCache(); + setIsDeletingCache(false); + + if (deleteLogsCacheError) { + notifications.toasts.addDanger({ + title: i18nTexts.errorToastTitle, + text: deleteLogsCacheError.message.toString(), + }); + return; + } + + const now = moment().toISOString(); + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_RESET_LOGS_COUNTER_CLICK); + setCheckpoint(now); + }; + + useEffect(() => { + // Loading shouldn't invalidate the previous state. + if (!isLoading) { + // An error should invalidate the previous state. + setHasNoDeprecationLogs(!error && !hasLogs); + } + // Depending upon setHasNoDeprecationLogs would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading, hasLogs]); + + if (isInitialRequest && isLoading) { + return ; + } + + if (error) { + return ( + +

+ {error.statusCode} - {error.message} +

+ + {i18nTexts.retryButton} + +
+ ); + } + + return ( + +

{i18nTexts.calloutBody}

+ + {i18nTexts.resetCounterButton} + +
+ ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/index.ts similarity index 76% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/index.ts index d4794623d8a99..e32655f90b848 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { getFixDeprecationLogsStep } from './fix_deprecation_logs_step'; +export { DeprecationsCountCheckpoint } from './deprecations_count_checkpoint'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.test.ts new file mode 100644 index 0000000000000..64a8920324d89 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getDeprecationIndexPatternId } from './external_links'; + +import { DEPRECATION_LOGS_INDEX_PATTERN } from '../../../../../common/constants'; +import { dataPluginMock, Start } from '../../../../../../../../src/plugins/data/public/mocks'; + +describe('External Links', () => { + let dataService: Start; + + beforeEach(() => { + dataService = dataPluginMock.createStartContract(); + }); + + describe('getDeprecationIndexPatternId', () => { + it('creates new index pattern if doesnt exist', async () => { + dataService.dataViews.find = jest.fn().mockResolvedValue([]); + dataService.dataViews.createAndSave = jest.fn().mockResolvedValue({ id: '123-456' }); + + const indexPatternId = await getDeprecationIndexPatternId(dataService); + + expect(indexPatternId).toBe('123-456'); + // prettier-ignore + expect(dataService.dataViews.createAndSave).toHaveBeenCalledWith({ + title: DEPRECATION_LOGS_INDEX_PATTERN, + allowNoIndex: true, + }, false, true); + }); + + it('uses existing index pattern if it already exists', async () => { + dataService.dataViews.find = jest.fn().mockResolvedValue([ + { + id: '123-456', + title: DEPRECATION_LOGS_INDEX_PATTERN, + }, + ]); + + const indexPatternId = await getDeprecationIndexPatternId(dataService); + + expect(indexPatternId).toBe('123-456'); + expect(dataService.dataViews.find).toHaveBeenCalledWith(DEPRECATION_LOGS_INDEX_PATTERN); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx new file mode 100644 index 0000000000000..dec43145ef966 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/external_links.tsx @@ -0,0 +1,175 @@ +/* + * 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 { encode } from 'rison-node'; +import React, { FunctionComponent, useState, useEffect } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiText } from '@elastic/eui'; + +import { DataPublicPluginStart } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; +import { + uiMetricService, + UIM_OBSERVABILITY_CLICK, + UIM_DISCOVER_CLICK, +} from '../../../lib/ui_metric'; + +import { + DEPRECATION_LOGS_INDEX_PATTERN, + DEPRECATION_LOGS_SOURCE_ID, +} from '../../../../../common/constants'; + +interface Props { + checkpoint: string; +} + +export const getDeprecationIndexPatternId = async (dataService: DataPublicPluginStart) => { + const results = await dataService.dataViews.find(DEPRECATION_LOGS_INDEX_PATTERN); + // Since the find might return also results with wildcard matchers we need to find the + // index pattern that has an exact match with our title. + const deprecationIndexPattern = results.find( + (result) => result.title === DEPRECATION_LOGS_INDEX_PATTERN + ); + + if (deprecationIndexPattern) { + return deprecationIndexPattern.id; + } else { + // When creating the index pattern, we need to be careful when creating an indexPattern + // for an index that doesnt exist. Since the deprecation logs data stream is only created + // when a deprecation log is indexed it could be possible that it might not exist at the + // time we need to render the DiscoveryAppLink. + // So in order to avoid those errors we need to make sure that the indexPattern is created + // with allowNoIndex and that we skip fetching fields to from the source index. + const override = false; + const skipFetchFields = true; + // prettier-ignore + const newIndexPattern = await dataService.dataViews.createAndSave({ + title: DEPRECATION_LOGS_INDEX_PATTERN, + allowNoIndex: true, + }, override, skipFetchFields); + + return newIndexPattern.id; + } +}; + +const DiscoverAppLink: FunctionComponent = ({ checkpoint }) => { + const { + services: { data: dataService }, + plugins: { share }, + } = useAppContext(); + + const [discoveryUrl, setDiscoveryUrl] = useState(); + + useEffect(() => { + const getDiscoveryUrl = async () => { + const indexPatternId = await getDeprecationIndexPatternId(dataService); + const locator = share.url.locators.get('DISCOVER_APP_LOCATOR'); + + if (!locator) { + return; + } + + const url = await locator.getUrl({ + indexPatternId, + query: { + language: 'kuery', + query: `@timestamp > "${checkpoint}"`, + }, + }); + + setDiscoveryUrl(url); + }; + + getDiscoveryUrl(); + }, [dataService, checkpoint, share.url.locators]); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_DISCOVER_CLICK); + }} + data-test-subj="viewDiscoverLogs" + > + + + ); +}; + +const ObservabilityAppLink: FunctionComponent = ({ checkpoint }) => { + const { + services: { + core: { http }, + }, + } = useAppContext(); + const logStreamUrl = http?.basePath?.prepend( + `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}&logPosition=(end:now,start:${encode( + checkpoint + )})` + ); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + { + uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_OBSERVABILITY_CLICK); + }} + data-test-subj="viewObserveLogs" + > + + + ); +}; + +export const ExternalLinks: FunctionComponent = ({ checkpoint }) => { + const { infra: hasInfraPlugin } = useAppContext().plugins; + + return ( + + {hasInfraPlugin && ( + + + +

+ +

+
+ + +
+
+ )} + + + +

+ +

+
+ + +
+
+
+ ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx new file mode 100644 index 0000000000000..a3e81f6edcd3a --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FunctionComponent, useState, useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText, EuiSpacer, EuiLink, EuiCallOut, EuiCode } from '@elastic/eui'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import { useAppContext } from '../../../app_context'; +import { ExternalLinks } from './external_links'; +import { DeprecationsCountCheckpoint } from './deprecations_count_checkpoint'; +import { useDeprecationLogging } from './use_deprecation_logging'; +import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; +import { loadLogsCheckpoint, saveLogsCheckpoint } from '../../../lib/logs_checkpoint'; +import type { OverviewStepProps } from '../../types'; +import { DEPRECATION_LOGS_INDEX } from '../../../../../common/constants'; +import { WithPrivileges, MissingPrivileges } from '../../../../shared_imports'; + +const i18nTexts = { + identifyStepTitle: i18n.translate('xpack.upgradeAssistant.overview.identifyStepTitle', { + defaultMessage: 'Identify deprecated API use and update your applications', + }), + analyzeTitle: i18n.translate('xpack.upgradeAssistant.overview.analyzeTitle', { + defaultMessage: 'Analyze deprecation logs', + }), + deprecationsCountCheckpointTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationsCountCheckpointTitle', + { + defaultMessage: 'Resolve deprecation issues and verify your changes', + } + ), + apiCompatibilityNoteTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.apiCompatibilityNoteTitle', + { + defaultMessage: 'Apply API compatibility headers (optional)', + } + ), + apiCompatibilityNoteBody: (docLink: string) => ( + + + + ), + }} + /> + ), + onlyLogWritingEnabledTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningTitle', + { + defaultMessage: 'Your logs are being written to the logs directory', + } + ), + onlyLogWritingEnabledBody: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationLogs.deprecationWarningBody', + { + defaultMessage: + 'Go to your logs directory to view the deprecation logs or enable deprecation log collection to see them in Kibana.', + } + ), + deniedPrivilegeTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.deprecationLogs.deniedPrivilegeTitle', + { + defaultMessage: 'You require index privileges to analyze the deprecation logs', + } + ), + deniedPrivilegeDescription: (privilegesMissing: MissingPrivileges) => ( + // NOTE: hardcoding the missing privilege because the WithPrivileges HOC + // doesnt provide a way to retrieve which specific privileges an index + // is missing. + {privilegesMissing?.index?.join(', ')} + ), + privilegesCount: privilegesMissing?.index?.length, + }} + /> + ), +}; + +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; + hasPrivileges: boolean; + privilegesMissing: MissingPrivileges; +} + +const FixLogsStep: FunctionComponent = ({ + setIsComplete, + hasPrivileges, + privilegesMissing, +}) => { + const { + services: { + core: { docLinks }, + }, + } = useAppContext(); + + const { + isDeprecationLogIndexingEnabled, + onlyDeprecationLogWritingEnabled, + isLoading, + isUpdating, + fetchError, + updateError, + resendRequest, + toggleLogging, + } = useDeprecationLogging(); + + const [checkpoint, setCheckpoint] = useState(loadLogsCheckpoint()); + + useEffect(() => { + saveLogsCheckpoint(checkpoint); + }, [checkpoint]); + + useEffect(() => { + if (!isDeprecationLogIndexingEnabled) { + setIsComplete(false); + } + + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDeprecationLogIndexingEnabled]); + + return ( + <> + + + {onlyDeprecationLogWritingEnabled && ( + <> + + +

{i18nTexts.onlyLogWritingEnabledBody}

+
+ + )} + + {!hasPrivileges && isDeprecationLogIndexingEnabled && ( + <> + + +

{i18nTexts.deniedPrivilegeDescription(privilegesMissing)}

+
+ + )} + + {hasPrivileges && isDeprecationLogIndexingEnabled && ( + <> + + +

{i18nTexts.analyzeTitle}

+
+ + + + + +

{i18nTexts.deprecationsCountCheckpointTitle}

+
+ + + + + +

{i18nTexts.apiCompatibilityNoteTitle}

+
+ + +

+ {i18nTexts.apiCompatibilityNoteBody( + docLinks.links.elasticsearch.apiCompatibilityHeader + )} +

+
+ + )} + + ); +}; + +export const getFixLogsStep = ({ isComplete, setIsComplete }: OverviewStepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + return { + status, + title: i18nTexts.identifyStepTitle, + 'data-test-subj': `fixLogsStep-${status}`, + children: ( + + {({ hasPrivileges, privilegesMissing, isLoading }) => ( + + )} + + ), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/index.ts similarity index 83% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/index.ts index daf2644c2477b..8a9a9faa6d098 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ESDeprecationStats } from './es_stats'; +export { getFixLogsStep } from './fix_logs_step'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/use_deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/use_deprecation_logging.ts similarity index 94% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/use_deprecation_logging.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/use_deprecation_logging.ts index 1aa34f2ec97c1..e25fd91ae2c52 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_deprecation_logs_step/use_deprecation_logging.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/use_deprecation_logging.ts @@ -9,8 +9,8 @@ import { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; +import { ResponseError } from '../../../../../common/types'; import { useAppContext } from '../../../app_context'; -import { ResponseError } from '../../../lib/api'; import { DeprecationLoggingPreviewProps } from '../../types'; const i18nTexts = { @@ -29,7 +29,12 @@ const i18nTexts = { }; export const useDeprecationLogging = (): DeprecationLoggingPreviewProps => { - const { api, notifications } = useAppContext(); + const { + services: { + api, + core: { notifications }, + }, + } = useAppContext(); const { data, error: fetchError, isLoading, resendRequest } = api.useLoadDeprecationLogging(); const [isDeprecationLogIndexingEnabled, setIsDeprecationLogIndexingEnabled] = useState(false); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx new file mode 100644 index 0000000000000..632994d4948a8 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/flyout.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { startCase } from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiLoadingSpinner, + EuiTitle, + EuiText, + EuiIcon, + EuiSpacer, + EuiInMemoryTable, +} from '@elastic/eui'; + +import { + SystemIndicesMigrationStatus, + SystemIndicesMigrationFeature, + MIGRATION_STATUS, +} from '../../../../../common/types'; + +export interface SystemIndicesFlyoutProps { + closeFlyout: () => void; + data: SystemIndicesMigrationStatus; +} + +const i18nTexts = { + closeButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.flyoutCloseButtonLabel', + { + defaultMessage: 'Close', + } + ), + flyoutTitle: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.flyoutTitle', { + defaultMessage: 'Migrate system indices', + }), + flyoutDescription: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.flyoutDescription', + { + defaultMessage: + 'Migrate the indices that store information for the following features before you upgrade.', + } + ), + migrationCompleteLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.migrationCompleteLabel', + { + defaultMessage: 'Migration complete', + } + ), + needsMigrationLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.needsMigrationLabel', + { + defaultMessage: 'Migration required', + } + ), + migratingLabel: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.migratingLabel', { + defaultMessage: 'Migration in progress', + }), + errorLabel: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.errorLabel', { + defaultMessage: 'Migration failed', + }), + featureNameTableColumn: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.featureNameTableColumn', + { + defaultMessage: 'Feature', + } + ), + statusTableColumn: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.statusTableColumn', + { + defaultMessage: 'Status', + } + ), +}; + +const renderMigrationStatus = (status: MIGRATION_STATUS) => { + if (status === 'NO_MIGRATION_NEEDED') { + return ( + + + + + + +

{i18nTexts.migrationCompleteLabel}

+
+
+
+ ); + } + + if (status === 'MIGRATION_NEEDED') { + return ( + +

{i18nTexts.needsMigrationLabel}

+
+ ); + } + + if (status === 'IN_PROGRESS') { + return ( + + + + + + +

{i18nTexts.migratingLabel}

+
+
+
+ ); + } + + if (status === 'ERROR') { + return ( + + + + + + +

{i18nTexts.errorLabel}

+
+
+
+ ); + } + + return ''; +}; + +const columns = [ + { + field: 'feature_name', + name: i18nTexts.featureNameTableColumn, + sortable: true, + truncateText: true, + render: (name: string) => startCase(name), + }, + { + field: 'migration_status', + name: i18nTexts.statusTableColumn, + sortable: true, + render: renderMigrationStatus, + }, +]; + +export const SystemIndicesFlyout = ({ closeFlyout, data }: SystemIndicesFlyoutProps) => { + return ( + <> + + +

{i18nTexts.flyoutTitle}

+
+
+ + +

{i18nTexts.flyoutDescription}

+
+ + + data-test-subj="featuresTable" + itemId="feature_name" + items={data.features} + columns={columns} + pagination={true} + sorting={true} + /> +
+ + + + + {i18nTexts.closeButtonLabel} + + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/index.ts similarity index 77% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/index.ts rename to x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/index.ts index 231b8ba2d7774..0be86929f2a43 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { getReviewLogsStep } from './review_logs_step'; +export { getMigrateSystemIndicesStep } from './migrate_system_indices'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx new file mode 100644 index 0000000000000..d14958148b2f8 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx @@ -0,0 +1,239 @@ +/* + * 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, { FunctionComponent, useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiText, + EuiButton, + EuiSpacer, + EuiIcon, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiCode, +} from '@elastic/eui'; +import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; + +import type { SystemIndicesMigrationFeature } from '../../../../../common/types'; +import type { OverviewStepProps } from '../../types'; +import { useMigrateSystemIndices } from './use_migrate_system_indices'; + +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; +} + +const getFailureCause = (features: SystemIndicesMigrationFeature[]) => { + const featureWithError = features.find((feature) => feature.migration_status === 'ERROR'); + + if (featureWithError) { + const indexWithError = featureWithError.indices.find((index) => index.failure_cause); + return { + feature: featureWithError?.feature_name, + failureCause: indexWithError?.failure_cause?.error.type, + }; + } + + return {}; +}; + +const i18nTexts = { + title: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.title', { + defaultMessage: 'Migrate system indices', + }), + bodyDescription: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.body', { + defaultMessage: 'Migrate the indices that store system information before you upgrade.', + }), + startButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.startButtonLabel', + { + defaultMessage: 'Migrate indices', + } + ), + inProgressButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.inProgressButtonLabel', + { + defaultMessage: 'Migration in progress', + } + ), + noMigrationNeeded: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.noMigrationNeeded', + { + defaultMessage: 'Migration complete', + } + ), + viewSystemIndicesStatus: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.viewSystemIndicesStatus', + { + defaultMessage: 'View migration details', + } + ), + retryButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.retryButtonLabel', + { + defaultMessage: 'Retry migration', + } + ), + loadingError: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.loadingError', { + defaultMessage: 'Could not retrieve the system indices status', + }), + migrationFailedTitle: i18n.translate( + 'xpack.upgradeAssistant.overview.systemIndices.migrationFailedTitle', + { + defaultMessage: 'System indices migration failed', + } + ), + migrationFailedBody: (features: SystemIndicesMigrationFeature[]) => { + const { feature, failureCause } = getFailureCause(features); + + return ( + {failureCause}, + }} + /> + ); + }, +}; + +const MigrateSystemIndicesStep: FunctionComponent = ({ setIsComplete }) => { + const { beginSystemIndicesMigration, startMigrationStatus, migrationStatus, setShowFlyout } = + useMigrateSystemIndices(); + + useEffect(() => { + setIsComplete(migrationStatus.data?.migration_status === 'NO_MIGRATION_NEEDED'); + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [migrationStatus.data?.migration_status]); + + if (migrationStatus.error) { + return ( + +

+ {migrationStatus.error.statusCode} - {migrationStatus.error.message} +

+ + {i18nTexts.retryButtonLabel} + +
+ ); + } + + if (migrationStatus.data?.migration_status === 'NO_MIGRATION_NEEDED') { + return ( + + + + + + +

{i18nTexts.noMigrationNeeded}

+
+
+
+ ); + } + + const isButtonDisabled = migrationStatus.isInitialRequest && migrationStatus.isLoading; + const isMigrating = migrationStatus.data?.migration_status === 'IN_PROGRESS'; + + return ( + <> + {startMigrationStatus.statusType === 'error' && ( + <> + + + + )} + + {migrationStatus.data?.migration_status === 'ERROR' && ( + <> + +

{i18nTexts.migrationFailedBody(migrationStatus.data?.features)}

+
+ + + )} + + + + + {isMigrating ? i18nTexts.inProgressButtonLabel : i18nTexts.startButtonLabel} + + + + setShowFlyout(true)} + isDisabled={isButtonDisabled} + data-test-subj="viewSystemIndicesStateButton" + > + {i18nTexts.viewSystemIndicesStatus} + + + + + ); +}; + +export const getMigrateSystemIndicesStep = ({ + isComplete, + setIsComplete, +}: OverviewStepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + + return { + title: i18nTexts.title, + status, + 'data-test-subj': `migrateSystemIndicesStep-${status}`, + children: ( + <> + +

{i18nTexts.bodyDescription}

+
+ + + + + + ), + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts new file mode 100644 index 0000000000000..d38e73562816e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/use_migrate_system_indices.ts @@ -0,0 +1,93 @@ +/* + * 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 { useCallback, useState, useEffect } from 'react'; +import useInterval from 'react-use/lib/useInterval'; + +import { SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS } from '../../../../../common/constants'; +import type { ResponseError } from '../../../../../common/types'; +import { GlobalFlyout } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; +import { SystemIndicesFlyout, SystemIndicesFlyoutProps } from './flyout'; + +const FLYOUT_ID = 'migrateSystemIndicesFlyout'; +const { useGlobalFlyout } = GlobalFlyout; + +export type StatusType = 'idle' | 'error' | 'started'; +interface MigrationStatus { + statusType: StatusType; + error?: ResponseError; +} + +export const useMigrateSystemIndices = () => { + const { + services: { api }, + } = useAppContext(); + + const [showFlyout, setShowFlyout] = useState(false); + + const [startMigrationStatus, setStartMigrationStatus] = useState({ + statusType: 'idle', + }); + + const { data, error, isLoading, resendRequest, isInitialRequest } = + api.useLoadSystemIndicesMigrationStatus(); + const isInProgress = data?.migration_status === 'IN_PROGRESS'; + + // We only want to poll for the status while the migration process is in progress. + useInterval(resendRequest, isInProgress ? SYSTEM_INDICES_MIGRATION_POLL_INTERVAL_MS : null); + + const { addContent: addContentToGlobalFlyout, removeContent: removeContentFromGlobalFlyout } = + useGlobalFlyout(); + + const closeFlyout = useCallback(() => { + setShowFlyout(false); + removeContentFromGlobalFlyout(FLYOUT_ID); + }, [removeContentFromGlobalFlyout]); + + useEffect(() => { + if (showFlyout) { + addContentToGlobalFlyout({ + id: FLYOUT_ID, + Component: SystemIndicesFlyout, + props: { + data: data!, + closeFlyout, + }, + flyoutProps: { + onClose: closeFlyout, + }, + }); + } + }, [addContentToGlobalFlyout, data, showFlyout, closeFlyout]); + + const beginSystemIndicesMigration = useCallback(async () => { + const { error: startMigrationError } = await api.migrateSystemIndices(); + + setStartMigrationStatus({ + statusType: startMigrationError ? 'error' : 'started', + error: startMigrationError ?? undefined, + }); + + if (!startMigrationError) { + resendRequest(); + } + }, [api, resendRequest]); + + return { + setShowFlyout, + startMigrationStatus, + beginSystemIndicesMigration, + migrationStatus: { + data, + error, + isLoading, + resendRequest, + isInitialRequest, + }, + }; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index f900416873b83..1e7961f8ea782 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent, useEffect } from 'react'; +import React, { FunctionComponent, useEffect, useState } from 'react'; import { EuiSteps, @@ -18,33 +18,53 @@ import { EuiPageContent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { useAppContext } from '../../app_context'; -import { getReviewLogsStep } from './review_logs_step'; -import { getFixDeprecationLogsStep } from './fix_deprecation_logs_step'; +import { uiMetricService, UIM_OVERVIEW_PAGE_LOAD } from '../../lib/ui_metric'; +import { getBackupStep } from './backup_step'; +import { getFixIssuesStep } from './fix_issues_step'; +import { getFixLogsStep } from './fix_logs_step'; import { getUpgradeStep } from './upgrade_step'; +import { getMigrateSystemIndicesStep } from './migrate_system_indices'; + +type OverviewStep = 'backup' | 'migrate_system_indices' | 'fix_issues' | 'fix_logs'; export const Overview: FunctionComponent = () => { - const { kibanaVersionInfo, breadcrumbs, docLinks, api } = useAppContext(); - const { nextMajor } = kibanaVersionInfo; + const { + services: { + breadcrumbs, + core: { docLinks }, + }, + plugins: { cloud }, + } = useAppContext(); useEffect(() => { - async function sendTelemetryData() { - await api.sendPageTelemetryData({ - overview: true, - }); - } - - sendTelemetryData(); - }, [api]); + uiMetricService.trackUiMetric(METRIC_TYPE.LOADED, UIM_OVERVIEW_PAGE_LOAD); + }, []); useEffect(() => { breadcrumbs.setBreadcrumbs('overview'); }, [breadcrumbs]); + const [completedStepsMap, setCompletedStepsMap] = useState({ + backup: false, + migrate_system_indices: false, + fix_issues: false, + fix_logs: false, + }); + + const isStepComplete = (step: OverviewStep) => completedStepsMap[step]; + const setCompletedStep = (step: OverviewStep, isCompleted: boolean) => { + setCompletedStepsMap({ + ...completedStepsMap, + [step]: isCompleted, + }); + }; + return ( - + { defaultMessage: 'Upgrade Assistant', })} description={i18n.translate('xpack.upgradeAssistant.overview.pageDescription', { - defaultMessage: 'Get ready for the next version of the Elastic Stack!', + defaultMessage: 'Get ready for the next version of Elastic!', })} rightSideItems={[ { @@ -83,9 +102,24 @@ export const Overview: FunctionComponent = () => { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_index.scss deleted file mode 100644 index 7eea518d5698e..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'stats_panel'; -@import 'no_deprecations/no_deprecations'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_stats_panel.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_stats_panel.scss deleted file mode 100644 index b32f3eb9ddbdf..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/_stats_panel.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Used by both es_stats and kibana_stats panel for having the EuiPopover Icon -// for errors shown next to the title without having to resort to wrapping everything -// with EuiFlexGroups. -.upgWarningIcon { - margin-left: $euiSizeS; -} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats.tsx deleted file mode 100644 index ef0b3f438da03..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; -import { useHistory } from 'react-router-dom'; - -import { - EuiStat, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiCard, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { getDeprecationsUpperLimit } from '../../../../lib/utils'; -import { useAppContext } from '../../../../app_context'; -import { EsStatsErrors } from './es_stats_error'; -import { NoDeprecations } from '../no_deprecations'; - -const i18nTexts = { - statsTitle: i18n.translate('xpack.upgradeAssistant.esDeprecationStats.statsTitle', { - defaultMessage: 'Elasticsearch', - }), - warningDeprecationsTitle: i18n.translate( - 'xpack.upgradeAssistant.esDeprecationStats.warningDeprecationsTitle', - { - defaultMessage: 'Warning', - } - ), - criticalDeprecationsTitle: i18n.translate( - 'xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsTitle', - { - defaultMessage: 'Critical', - } - ), - loadingText: i18n.translate('xpack.upgradeAssistant.esDeprecationStats.loadingText', { - defaultMessage: 'Loading Elasticsearch deprecation stats…', - }), - getCriticalDeprecationsMessage: (criticalDeprecations: number) => - i18n.translate('xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsLabel', { - defaultMessage: 'This cluster has {criticalDeprecations} critical deprecations', - values: { - criticalDeprecations, - }, - }), - getWarningDeprecationMessage: (warningDeprecations: number) => - i18n.translate('xpack.upgradeAssistant.esDeprecationStats.warningDeprecationsTooltip', { - defaultMessage: - 'This cluster has {warningDeprecations} non-critical {warningDeprecations, plural, one {deprecation} other {deprecations}}', - values: { - warningDeprecations, - }, - }), -}; - -export const ESDeprecationStats: FunctionComponent = () => { - const history = useHistory(); - const { api } = useAppContext(); - - const { data: esDeprecations, isLoading, error } = api.useLoadEsDeprecations(); - - const warningDeprecations = - esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) || []; - const criticalDeprecations = - esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical) || []; - - const hasWarnings = warningDeprecations.length > 0; - const hasCritical = criticalDeprecations.length > 0; - const hasNoDeprecations = !isLoading && !error && !hasWarnings && !hasCritical; - const shouldRenderStat = (forSection: boolean) => error || isLoading || forSection; - - return ( - - {i18nTexts.statsTitle} - {error && } - - } - {...(!hasNoDeprecations && reactRouterNavigate(history, '/es_deprecations'))} - > - - - {hasNoDeprecations && ( - - - - )} - - {shouldRenderStat(hasCritical) && ( - - - {error === null && ( - -

- {isLoading - ? i18nTexts.loadingText - : i18nTexts.getCriticalDeprecationsMessage(criticalDeprecations.length)} -

-
- )} -
-
- )} - - {shouldRenderStat(hasWarnings) && ( - - - {!error && ( - -

- {isLoading - ? i18nTexts.loadingText - : i18nTexts.getWarningDeprecationMessage(warningDeprecations.length)} -

-
- )} -
-
- )} -
-
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats_error.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats_error.tsx deleted file mode 100644 index c717a8a2e12e8..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/es_stats/es_stats_error.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiIconTip } from '@elastic/eui'; -import { ResponseError } from '../../../../lib/api'; -import { getEsDeprecationError } from '../../../../lib/get_es_deprecation_error'; - -interface Props { - error: ResponseError; -} - -export const EsStatsErrors: React.FunctionComponent = ({ error }) => { - let iconContent: React.ReactNode; - - const { code: errorType, message } = getEsDeprecationError(error); - - switch (errorType) { - case 'unauthorized_error': - iconContent = ( - - ); - break; - case 'partially_upgraded_error': - iconContent = ( - - ); - break; - case 'upgraded_error': - iconContent = ( - - ); - break; - case 'request_error': - default: - iconContent = ( - - ); - } - - return {iconContent}; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/kibana_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/kibana_stats.tsx deleted file mode 100644 index d7b820aa4a484..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/kibana_stats.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent, useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; - -import { - EuiCard, - EuiStat, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import type { DomainDeprecationDetails } from 'kibana/public'; -import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { getDeprecationsUpperLimit } from '../../../../lib/utils'; -import { useAppContext } from '../../../../app_context'; -import { NoDeprecations } from '../no_deprecations'; - -const i18nTexts = { - statsTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.statsTitle', { - defaultMessage: 'Kibana', - }), - warningDeprecationsTitle: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationStats.warningDeprecationsTitle', - { - defaultMessage: 'Warning', - } - ), - criticalDeprecationsTitle: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsTitle', - { - defaultMessage: 'Critical', - } - ), - loadingError: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecationStats.loadingErrorMessage', - { - defaultMessage: 'An error occurred while retrieving Kibana deprecations.', - } - ), - loadingText: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.loadingText', { - defaultMessage: 'Loading Kibana deprecation stats…', - }), - getCriticalDeprecationsMessage: (criticalDeprecations: number) => - i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsLabel', { - defaultMessage: - 'Kibana has {criticalDeprecations} critical {criticalDeprecations, plural, one {deprecation} other {deprecations}}', - values: { - criticalDeprecations, - }, - }), - getWarningDeprecationsMessage: (warningDeprecations: number) => - i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.getWarningDeprecationsMessage', { - defaultMessage: - 'Kibana has {warningDeprecations} warning {warningDeprecations, plural, one {deprecation} other {deprecations}}', - values: { - warningDeprecations, - }, - }), -}; - -export const KibanaDeprecationStats: FunctionComponent = () => { - const history = useHistory(); - const { deprecations } = useAppContext(); - - const [kibanaDeprecations, setKibanaDeprecations] = useState< - DomainDeprecationDetails[] | undefined - >(undefined); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - useEffect(() => { - async function getAllDeprecations() { - setIsLoading(true); - - try { - const response = await deprecations.getAllDeprecations(); - setKibanaDeprecations(response); - } catch (e) { - setError(e); - } - - setIsLoading(false); - } - - getAllDeprecations(); - }, [deprecations]); - - const warningDeprecationsCount = - kibanaDeprecations?.filter((deprecation) => deprecation.level === 'warning')?.length ?? 0; - const criticalDeprecationsCount = - kibanaDeprecations?.filter((deprecation) => deprecation.level === 'critical')?.length ?? 0; - - const hasCritical = criticalDeprecationsCount > 0; - const hasWarnings = warningDeprecationsCount > 0; - const hasNoDeprecations = !isLoading && !error && !hasWarnings && !hasCritical; - const shouldRenderStat = (forSection: boolean) => error || isLoading || forSection; - - return ( - - {i18nTexts.statsTitle} - {error && ( - - )} - - } - {...(!hasNoDeprecations && reactRouterNavigate(history, '/kibana_deprecations'))} - > - - - {hasNoDeprecations && ( - - - - )} - - {shouldRenderStat(hasCritical) && ( - - - {error === undefined && ( - -

- {isLoading - ? i18nTexts.loadingText - : i18nTexts.getCriticalDeprecationsMessage(criticalDeprecationsCount)} -

-
- )} -
-
- )} - - {shouldRenderStat(hasWarnings) && ( - - - {!error && ( - -

- {isLoading - ? i18nTexts.loadingText - : i18nTexts.getWarningDeprecationsMessage(warningDeprecationsCount)} -

-
- )} -
-
- )} -
-
- ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/_no_deprecations.scss b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/_no_deprecations.scss deleted file mode 100644 index 0697efbd6ee37..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/_no_deprecations.scss +++ /dev/null @@ -1,3 +0,0 @@ -.upgRenderSuccessMessage { - margin-top: $euiSizeL; -} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/no_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/no_deprecations.tsx deleted file mode 100644 index 06fea677aa0a5..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/no_deprecations/no_deprecations.tsx +++ /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. - */ - -import React, { FunctionComponent } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -const i18nTexts = { - noDeprecationsText: i18n.translate( - 'xpack.upgradeAssistant.esDeprecationStats.noDeprecationsText', - { - defaultMessage: 'No warnings. Good to go!', - } - ), -}; - -export const NoDeprecations: FunctionComponent = () => { - return ( - - - - - - - {i18nTexts.noDeprecationsText} - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/review_logs_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/review_logs_step.tsx deleted file mode 100644 index 4ebde8b5f847a..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/review_logs_step.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import { ESDeprecationStats } from './es_stats'; -import { KibanaDeprecationStats } from './kibana_stats'; - -const i18nTexts = { - reviewStepTitle: i18n.translate('xpack.upgradeAssistant.overview.reviewStepTitle', { - defaultMessage: 'Review deprecated settings and resolve issues', - }), -}; - -export const getReviewLogsStep = ({ nextMajor }: { nextMajor: number }): EuiStepProps => { - return { - title: i18nTexts.reviewStepTitle, - status: 'incomplete', - children: ( - <> - -

- -

-
- - - - - - - - - - - - - - ), - }; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx index d66a408cfce77..b3a3179ed9079 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx @@ -17,24 +17,21 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import type { DocLinksStart } from 'src/core/public'; -import { useKibana } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; const i18nTexts = { - upgradeStepTitle: (nextMajor: number) => - i18n.translate('xpack.upgradeAssistant.overview.upgradeStepTitle', { - defaultMessage: 'Install {nextMajor}.0', - values: { nextMajor }, - }), + upgradeStepTitle: i18n.translate('xpack.upgradeAssistant.overview.upgradeStepTitle', { + defaultMessage: 'Upgrade to Elastic 8.x', + }), upgradeStepDescription: i18n.translate('xpack.upgradeAssistant.overview.upgradeStepDescription', { defaultMessage: - "Once you've resolved all critical issues and verified that your applications are ready, you can upgrade the Elastic Stack.", + 'Once you’ve resolved all critical issues and verified that your applications are ready, you can upgrade to Elastic 8.x. Be sure to back up your data again before upgrading.', }), upgradeStepDescriptionForCloud: i18n.translate( 'xpack.upgradeAssistant.overview.upgradeStepDescriptionForCloud', { defaultMessage: - "Once you've resolved all critical issues and verified that your applications are ready, you can upgrade the Elastic Stack. Upgrade your deployment on Elastic Cloud.", + "Once you've resolved all critical issues and verified that your applications are ready, you can upgrade to Elastic 8.x. Be sure to back up your data again before upgrading. Upgrade your deployment on Elastic Cloud.", } ), upgradeStepLink: i18n.translate('xpack.upgradeAssistant.overview.upgradeStepLink', { @@ -48,20 +45,23 @@ const i18nTexts = { }), }; -const UpgradeStep = ({ docLinks }: { docLinks: DocLinksStart }) => { - const { cloud } = useKibana().services; - +const UpgradeStep = () => { + const { + plugins: { cloud }, + services: { + core: { docLinks }, + }, + } = useAppContext(); const isCloudEnabled: boolean = Boolean(cloud?.isCloudEnabled); - const cloudDeploymentUrl: string = `${cloud?.baseUrl ?? ''}/deployments/${cloud?.cloudId ?? ''}`; - let callToAction; if (isCloudEnabled) { + const upgradeOnCloudUrl = cloud!.deploymentUrl + '?show_upgrade=true'; callToAction = ( { { } else { callToAction = ( { ); }; -interface Props { - docLinks: DocLinksStart; - nextMajor: number; -} - -export const getUpgradeStep = ({ docLinks, nextMajor }: Props): EuiStepProps => { +export const getUpgradeStep = (): EuiStepProps => { return { - title: i18nTexts.upgradeStepTitle(nextMajor), + title: i18nTexts.upgradeStepTitle, status: 'incomplete', - children: , + 'data-test-subj': 'upgradeStep', + children: , }; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_badge.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_badge.tsx new file mode 100644 index 0000000000000..c0b8f0eb24304 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_badge.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, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; + +const i18nTexts = { + criticalBadgeLabel: i18n.translate('xpack.upgradeAssistant.deprecationBadge.criticalBadgeLabel', { + defaultMessage: 'Critical', + }), + resolvedBadgeLabel: i18n.translate('xpack.upgradeAssistant.deprecationBadge.resolvedBadgeLabel', { + defaultMessage: 'Resolved', + }), + warningBadgeLabel: i18n.translate('xpack.upgradeAssistant.deprecationBadge.warningBadgeLabel', { + defaultMessage: 'Warning', + }), +}; + +interface Props { + isCritical: boolean; + isResolved?: boolean; +} + +export const DeprecationBadge: FunctionComponent = ({ isCritical, isResolved }) => { + if (isResolved) { + return ( + + {i18nTexts.resolvedBadgeLabel} + + ); + } + + if (isCritical) { + return ( + + {i18nTexts.criticalBadgeLabel} + + ); + } + + return ( + + {i18nTexts.warningBadgeLabel} + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx new file mode 100644 index 0000000000000..32d214f0d80f2 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx @@ -0,0 +1,71 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { LevelInfoTip } from './level_info_tip'; + +const i18nTexts = { + getCriticalStatusLabel: (count: number) => + i18n.translate('xpack.upgradeAssistant.deprecationCount.criticalStatusLabel', { + defaultMessage: 'Critical: {count}', + values: { + count, + }, + }), + getWarningStatusLabel: (count: number) => + i18n.translate('xpack.upgradeAssistant.deprecationCount.warningStatusLabel', { + defaultMessage: 'Warning: {count}', + values: { + count, + }, + }), +}; + +interface Props { + totalCriticalDeprecations: number; + totalWarningDeprecations: number; +} + +export const DeprecationCount: FunctionComponent = ({ + totalCriticalDeprecations, + totalWarningDeprecations, +}) => { + return ( + + + + + + {i18nTexts.getCriticalStatusLabel(totalCriticalDeprecations)} + + + + + + + + + + + + + + {i18nTexts.getWarningStatusLabel(totalWarningDeprecations)} + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx new file mode 100644 index 0000000000000..da8c83597f7e2 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; + +interface Props { + documentationUrl?: string; +} + +export const DeprecationFlyoutLearnMoreLink = ({ documentationUrl }: Props) => { + return ( + + {i18n.translate('xpack.upgradeAssistant.deprecationFlyout.learnMoreLinkLabel', { + defaultMessage: 'Learn more', + })} + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx deleted file mode 100644 index 709ef7224870e..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/count_summary.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; - -import { EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const DeprecationCountSummary: FunctionComponent<{ - allDeprecationsCount: number; - filteredDeprecationsCount: number; -}> = ({ filteredDeprecationsCount, allDeprecationsCount }) => ( - - {allDeprecationsCount > 0 ? ( - - ) : ( - - )} - {filteredDeprecationsCount !== allDeprecationsCount && ( - <> - {'. '} - - - )} - -); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx deleted file mode 100644 index 6cb5ae3675c44..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/deprecation_list_bar.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { DeprecationCountSummary } from './count_summary'; - -const i18nTexts = { - expandAllButton: i18n.translate( - 'xpack.upgradeAssistant.deprecationListBar.expandAllButtonLabel', - { - defaultMessage: 'Expand all', - } - ), - collapseAllButton: i18n.translate( - 'xpack.upgradeAssistant.deprecationListBar.collapseAllButtonLabel', - { - defaultMessage: 'Collapse all', - } - ), -}; - -export const DeprecationListBar: FunctionComponent<{ - allDeprecationsCount: number; - filteredDeprecationsCount: number; - setExpandAll: (shouldExpandAll: boolean) => void; -}> = ({ allDeprecationsCount, filteredDeprecationsCount, setExpandAll }) => { - return ( - - - - - - - - - setExpandAll(true)} - data-test-subj="expandAll" - > - {i18nTexts.expandAllButton} - - - - setExpandAll(false)} - data-test-subj="collapseAll" - > - {i18nTexts.collapseAllButton} - - - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/index.ts deleted file mode 100644 index cbc04fd86bfbd..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_list_bar/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 { DeprecationListBar } from './deprecation_list_bar'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx deleted file mode 100644 index ae2c0ba1c4877..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_pagination.tsx +++ /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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui'; - -export const DeprecationPagination: FunctionComponent<{ - pageCount: number; - activePage: number; - setPage: (page: number) => void; -}> = ({ pageCount, activePage, setPage }) => { - return ( - - - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx new file mode 100644 index 0000000000000..01cf950abbc31 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecations_page_loading_error.tsx @@ -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 React, { FunctionComponent } from 'react'; +import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { DeprecationSource } from '../../../../common/types'; + +interface Props { + deprecationSource: DeprecationSource; + message?: string; +} + +export const DeprecationsPageLoadingError: FunctionComponent = ({ + deprecationSource, + message, +}) => ( + + + {i18n.translate('xpack.upgradeAssistant.deprecationsPageLoadingError.title', { + defaultMessage: 'Could not retrieve {deprecationSource} deprecation issues', + values: { deprecationSource }, + })} + + } + body={message} + /> + +); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx deleted file mode 100644 index 9bf35668ac88a..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/health.tsx +++ /dev/null @@ -1,88 +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 { countBy } from 'lodash'; -import React, { FunctionComponent } from 'react'; - -import { EuiBadge, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { DeprecationInfo } from '../../../../common/types'; -import { COLOR_MAP, REVERSE_LEVEL_MAP } from '../constants'; - -const LocalizedLevels: { [level: string]: string } = { - warning: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.warningLabel', { - defaultMessage: 'Warning', - }), - critical: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel', { - defaultMessage: 'Critical', - }), -}; - -export const LocalizedActions: { [level: string]: string } = { - warning: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.warningActionTooltip', { - defaultMessage: 'Resolving this issue before upgrading is advised, but not required.', - }), - critical: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip', { - defaultMessage: 'Resolve this issue before upgrading.', - }), -}; - -interface DeprecationHealthProps { - deprecationLevels: number[]; - single?: boolean; -} - -const SingleHealth: FunctionComponent<{ level: DeprecationInfo['level']; label: string }> = ({ - level, - label, -}) => ( - - - {label} - -   - -); - -/** - * Displays a summary health for a list of deprecations that shows the number and level of severity - * deprecations in the list. - */ -export const DeprecationHealth: FunctionComponent = ({ - deprecationLevels, - single = false, -}) => { - if (deprecationLevels.length === 0) { - return ; - } - - if (single) { - const highest = Math.max(...deprecationLevels); - const highestLevel = REVERSE_LEVEL_MAP[highest]; - - return ; - } - - const countByLevel = countBy(deprecationLevels); - - return ( - - {Object.keys(countByLevel) - .map((k) => parseInt(k, 10)) - .sort() - .map((level) => [level, REVERSE_LEVEL_MAP[level]]) - .map(([numLevel, stringLevel]) => ( - - ))} - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts index c79d8247a93f1..34496e1e8eb55 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts @@ -6,7 +6,8 @@ */ export { NoDeprecationsPrompt } from './no_deprecations'; -export { DeprecationHealth } from './health'; -export { SearchBar } from './search_bar'; -export { DeprecationPagination } from './deprecation_pagination'; -export { DeprecationListBar } from './deprecation_list_bar'; +export { DeprecationCount } from './deprecation_count'; +export { DeprecationBadge } from './deprecation_badge'; +export { DeprecationsPageLoadingError } from './deprecations_page_loading_error'; +export { DeprecationFlyoutLearnMoreLink } from './deprecation_flyout_learn_more_link'; +export { LevelInfoTip } from './level_info_tip'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx new file mode 100644 index 0000000000000..d3600a7290b4e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx @@ -0,0 +1,27 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIconTip } from '@elastic/eui'; + +const i18nTexts = { + critical: i18n.translate('xpack.upgradeAssistant.levelInfoTip.criticalLabel', { + defaultMessage: 'Critical issues must be resolved before you upgrade', + }), + warning: i18n.translate('xpack.upgradeAssistant.levelInfoTip.warningLabel', { + defaultMessage: 'Warning issues can be ignored at your discretion', + }), +}; + +interface Props { + level: 'critical' | 'warning'; +} + +export const LevelInfoTip: FunctionComponent = ({ level }) => { + return ; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap deleted file mode 100644 index 5a8619e1e687b..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/group_by_filter.test.tsx.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GroupByFilter renders 1`] = ` - - - - By issue - - - By index - - - -`; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap deleted file mode 100644 index 551e212f23dd7..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/__snapshots__/level_filter.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DeprecationLevelFilter renders 1`] = ` - - - - Critical - - - -`; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx deleted file mode 100644 index fa863e4935c09..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.test.tsx +++ /dev/null @@ -1,31 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { GroupByOption } from '../../types'; -import { GroupByFilter } from './group_by_filter'; - -const defaultProps = { - availableGroupByOptions: [GroupByOption.message, GroupByOption.index], - currentGroupBy: GroupByOption.message, - onGroupByChange: jest.fn(), -}; - -describe('GroupByFilter', () => { - test('renders', () => { - expect(shallow()).toMatchSnapshot(); - }); - - test('clicking button calls onGroupByChange', () => { - const wrapper = mount(); - wrapper.find('button.euiFilterButton-hasActiveFilters').simulate('click'); - expect(defaultProps.onGroupByChange).toHaveBeenCalledTimes(1); - expect(defaultProps.onGroupByChange.mock.calls[0][0]).toEqual(GroupByOption.message); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx deleted file mode 100644 index c37ae47793b95..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/group_by_filter.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { GroupByOption } from '../../types'; - -const LocalizedOptions: { [option: string]: string } = { - message: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel', { - defaultMessage: 'By issue', - }), - index: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel', { - defaultMessage: 'By index', - }), -}; - -interface GroupByFilterProps { - availableGroupByOptions: GroupByOption[]; - currentGroupBy: GroupByOption; - onGroupByChange: (groupBy: GroupByOption) => void; -} - -export const GroupByFilter: React.FunctionComponent = ({ - availableGroupByOptions, - currentGroupBy, - onGroupByChange, -}) => { - if (availableGroupByOptions.length <= 1) { - return null; - } - - return ( - - - {availableGroupByOptions.map((option) => ( - - {LocalizedOptions[option]} - - ))} - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx deleted file mode 100644 index c778e56e8df11..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.test.tsx +++ /dev/null @@ -1,34 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; -import { LevelFilterOption } from '../../types'; - -import { DeprecationLevelFilter } from './level_filter'; - -const defaultProps = { - levelsCount: { - warning: 4, - critical: 1, - }, - currentFilter: 'all' as LevelFilterOption, - onFilterChange: jest.fn(), -}; - -describe('DeprecationLevelFilter', () => { - test('renders', () => { - expect(shallow()).toMatchSnapshot(); - }); - - test('clicking button calls onFilterChange', () => { - const wrapper = mount(); - wrapper.find('button[data-test-subj="criticalLevelFilter"]').simulate('click'); - expect(defaultProps.onFilterChange).toHaveBeenCalledTimes(1); - expect(defaultProps.onFilterChange.mock.calls[0][0]).toEqual('critical'); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx deleted file mode 100644 index 59bfaa595b0a6..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/level_filter.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { LevelFilterOption } from '../../types'; - -const LocalizedOptions: { [option: string]: string } = { - critical: i18n.translate( - 'xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel', - { defaultMessage: 'Critical' } - ), -}; -interface DeprecationLevelProps { - levelsCount: { - [key: string]: number; - }; - currentFilter: LevelFilterOption; - onFilterChange(level: LevelFilterOption): void; -} - -export const DeprecationLevelFilter: React.FunctionComponent = ({ - levelsCount, - currentFilter, - onFilterChange, -}) => { - return ( - - - { - onFilterChange(currentFilter !== 'critical' ? 'critical' : 'all'); - }} - hasActiveFilters={currentFilter === 'critical'} - numFilters={levelsCount.critical || undefined} - data-test-subj="criticalLevelFilter" - > - {LocalizedOptions.critical} - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx deleted file mode 100644 index 7c805398a6b47..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/search_bar/search_bar.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButton, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiCallOut, - EuiSpacer, -} from '@elastic/eui'; - -import type { DomainDeprecationDetails } from 'kibana/public'; -import { DeprecationInfo } from '../../../../../common/types'; -import { validateRegExpString } from '../../../lib/utils'; -import { GroupByOption, LevelFilterOption } from '../../types'; -import { DeprecationLevelFilter } from './level_filter'; -import { GroupByFilter } from './group_by_filter'; - -interface SearchBarProps { - allDeprecations?: DeprecationInfo[] | DomainDeprecationDetails; - isLoading: boolean; - loadData: () => void; - currentFilter: LevelFilterOption; - onFilterChange: (filter: LevelFilterOption) => void; - onSearchChange: (filter: string) => void; - totalDeprecationsCount: number; - levelToDeprecationCountMap: { - [key: string]: number; - }; - groupByFilterProps?: { - availableGroupByOptions: GroupByOption[]; - currentGroupBy: GroupByOption; - onGroupByChange: (groupBy: GroupByOption) => void; - }; -} - -const i18nTexts = { - searchAriaLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationListSearchBar.placeholderAriaLabel', - { defaultMessage: 'Filter' } - ), - searchPlaceholderLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationListSearchBar.placeholderLabel', - { - defaultMessage: 'Filter', - } - ), - reloadButtonLabel: i18n.translate( - 'xpack.upgradeAssistant.deprecationListSearchBar.reloadButtonLabel', - { - defaultMessage: 'Reload', - } - ), - getInvalidSearchMessage: (searchTermError: string) => - i18n.translate('xpack.upgradeAssistant.deprecationListSearchBar.filterErrorMessageLabel', { - defaultMessage: 'Filter invalid: {searchTermError}', - values: { searchTermError }, - }), -}; - -export const SearchBar: FunctionComponent = ({ - totalDeprecationsCount, - levelToDeprecationCountMap, - isLoading, - loadData, - currentFilter, - onFilterChange, - onSearchChange, - groupByFilterProps, -}) => { - const [searchTermError, setSearchTermError] = useState(null); - const filterInvalid = Boolean(searchTermError); - return ( - <> - - - - - { - const string = e.target.value; - const errorMessage = validateRegExpString(string); - if (errorMessage) { - // Emit an empty search term to listeners if search term is invalid. - onSearchChange(''); - setSearchTermError(errorMessage); - } else { - onSearchChange(e.target.value); - if (searchTermError) { - setSearchTermError(null); - } - } - }} - /> - - - {/* These two components provide their own EuiFlexItem wrappers */} - - {groupByFilterProps && } - - - - - {i18nTexts.reloadButtonLabel} - - - - - {filterInvalid && ( - <> - - - - - )} - - - - ); -}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/types.ts b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts index b46bb583244f0..637c48cc61403 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ResponseError } from '../lib/api'; +import { ResponseError } from '../../../common/types'; export enum LoadingState { Loading, @@ -13,12 +13,11 @@ export enum LoadingState { Error, } -export type LevelFilterOption = 'all' | 'critical'; - -export enum GroupByOption { - message = 'message', - index = 'index', - node = 'node', +export enum CancelLoadingState { + Requested, + Loading, + Success, + Error, } export type DeprecationTableColumns = @@ -39,3 +38,8 @@ export interface DeprecationLoggingPreviewProps { resendRequest: () => void; toggleLogging: () => void; } + +export interface OverviewStepProps { + isComplete: boolean; + setIsComplete: (isComplete: boolean) => void; +} diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts index 78070c5717496..8b967d994af9b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/api.ts @@ -6,8 +6,20 @@ */ import { HttpSetup } from 'src/core/public'; -import { ESUpgradeStatus } from '../../../common/types'; -import { API_BASE_PATH } from '../../../common/constants'; + +import { + ESUpgradeStatus, + CloudBackupStatus, + ClusterUpgradeState, + ResponseError, + SystemIndicesMigrationStatus, +} from '../../../common/types'; +import { + API_BASE_PATH, + CLUSTER_UPGRADE_STATUS_POLL_INTERVAL_MS, + DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, + CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS, +} from '../../../common/constants'; import { UseRequestConfig, SendRequestConfig, @@ -16,52 +28,103 @@ import { useRequest as _useRequest, } from '../../shared_imports'; -export interface ResponseError { - statusCode: number; - message: string | Error; - attributes?: Record; -} +type ClusterUpgradeStateListener = (clusterUpgradeState: ClusterUpgradeState) => void; export class ApiService { private client: HttpSetup | undefined; + private clusterUpgradeStateListeners: ClusterUpgradeStateListener[] = []; + + private handleClusterUpgradeError(error: ResponseError | null) { + const isClusterUpgradeError = Boolean(error && error.statusCode === 426); + if (isClusterUpgradeError) { + const clusterUpgradeState = error!.attributes!.allNodesUpgraded + ? 'isUpgradeComplete' + : 'isUpgrading'; + this.clusterUpgradeStateListeners.forEach((listener) => listener(clusterUpgradeState)); + } + } - private useRequest(config: UseRequestConfig) { + private useRequest(config: UseRequestConfig) { if (!this.client) { - throw new Error('API service has not be initialized.'); + throw new Error('API service has not been initialized.'); } - return _useRequest(this.client, config); + const response = _useRequest(this.client, config); + // NOTE: This will cause an infinite render loop in any component that both + // consumes the hook calling this useRequest function and also handles + // cluster upgrade errors. Note that sendRequest doesn't have this problem. + // + // This is due to React's fundamental expectation that hooks be idempotent, + // so it can render a component as many times as necessary and thereby call + // the hook on each render without worrying about that triggering subsequent + // renders. + // + // In this case we call handleClusterUpgradeError every time useRequest is + // called, which is on every render. If handling the cluster upgrade error + // causes a state change in the consuming component, that will trigger a + // render, which will call useRequest again, calling handleClusterUpgradeError, + // causing a state change in the consuming component, and so on. + this.handleClusterUpgradeError(response.error); + return response; } - private sendRequest( + private async sendRequest( config: SendRequestConfig - ): Promise> { + ): Promise> { if (!this.client) { - throw new Error('API service has not be initialized.'); + throw new Error('API service has not been initialized.'); } - return _sendRequest(this.client, config); + const response = await _sendRequest(this.client, config); + this.handleClusterUpgradeError(response.error); + return response; } public setup(httpClient: HttpSetup): void { this.client = httpClient; } - public useLoadEsDeprecations() { - return this.useRequest({ - path: `${API_BASE_PATH}/es_deprecations`, + public onClusterUpgradeStateChange(listener: ClusterUpgradeStateListener) { + this.clusterUpgradeStateListeners.push(listener); + } + + public useLoadClusterUpgradeStatus() { + return this.useRequest({ + path: `${API_BASE_PATH}/cluster_upgrade_status`, + method: 'get', + pollIntervalMs: CLUSTER_UPGRADE_STATUS_POLL_INTERVAL_MS, + }); + } + + public useLoadCloudBackupStatus() { + return this.useRequest({ + path: `${API_BASE_PATH}/cloud_backup_status`, method: 'get', + pollIntervalMs: CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS, }); } - public async sendPageTelemetryData(telemetryData: { [tabName: string]: boolean }) { + public useLoadSystemIndicesMigrationStatus() { + return this.useRequest({ + path: `${API_BASE_PATH}/system_indices_migration`, + method: 'get', + }); + } + + public async migrateSystemIndices() { const result = await this.sendRequest({ - path: `${API_BASE_PATH}/stats/ui_open`, - method: 'put', - body: JSON.stringify(telemetryData), + path: `${API_BASE_PATH}/system_indices_migration`, + method: 'post', }); return result; } + public useLoadEsDeprecations() { + return this.useRequest({ + path: `${API_BASE_PATH}/es_deprecations`, + method: 'get', + }); + } + public useLoadDeprecationLogging() { return this.useRequest<{ isDeprecationLogIndexingEnabled: boolean; @@ -73,44 +136,54 @@ export class ApiService { } public async updateDeprecationLogging(loggingData: { isEnabled: boolean }) { - const result = await this.sendRequest({ + return await this.sendRequest({ path: `${API_BASE_PATH}/deprecation_logging`, method: 'put', body: JSON.stringify(loggingData), }); + } - return result; + public getDeprecationLogsCount(from: string) { + return this.useRequest<{ + count: number; + }>({ + path: `${API_BASE_PATH}/deprecation_logging/count`, + method: 'get', + query: { from }, + pollIntervalMs: DEPRECATION_LOGS_COUNT_POLL_INTERVAL_MS, + }); + } + + public deleteDeprecationLogsCache() { + return this.sendRequest({ + path: `${API_BASE_PATH}/deprecation_logging/cache`, + method: 'delete', + }); } public async updateIndexSettings(indexName: string, settings: string[]) { - const result = await this.sendRequest({ + return await this.sendRequest({ path: `${API_BASE_PATH}/${indexName}/index_settings`, method: 'post', body: { settings: JSON.stringify(settings), }, }); - - return result; } public async upgradeMlSnapshot(body: { jobId: string; snapshotId: string }) { - const result = await this.sendRequest({ + return await this.sendRequest({ path: `${API_BASE_PATH}/ml_snapshots`, method: 'post', body, }); - - return result; } public async deleteMlSnapshot({ jobId, snapshotId }: { jobId: string; snapshotId: string }) { - const result = await this.sendRequest({ + return await this.sendRequest({ path: `${API_BASE_PATH}/ml_snapshots/${jobId}/${snapshotId}`, method: 'delete', }); - - return result; } public async getMlSnapshotUpgradeStatus({ @@ -126,14 +199,13 @@ export class ApiService { }); } - public async sendReindexTelemetryData(telemetryData: { [key: string]: boolean }) { - const result = await this.sendRequest({ - path: `${API_BASE_PATH}/stats/ui_reindex`, - method: 'put', - body: JSON.stringify(telemetryData), + public useLoadMlUpgradeMode() { + return this.useRequest<{ + mlUpgradeModeEnabled: boolean; + }>({ + path: `${API_BASE_PATH}/ml_upgrade_mode`, + method: 'get', }); - - return result; } public async getReindexStatus(indexName: string) { diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts index f36dc2096ddc7..3e30ffd06db15 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/breadcrumbs.ts @@ -16,12 +16,12 @@ const i18nTexts = { defaultMessage: 'Upgrade Assistant', }), esDeprecations: i18n.translate('xpack.upgradeAssistant.breadcrumb.esDeprecationsLabel', { - defaultMessage: 'Elasticsearch deprecation warnings', + defaultMessage: 'Elasticsearch deprecation issues', }), kibanaDeprecations: i18n.translate( 'xpack.upgradeAssistant.breadcrumb.kibanaDeprecationsLabel', { - defaultMessage: 'Kibana deprecations', + defaultMessage: 'Kibana deprecation issues', } ), }, diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts index 85cfd2a3fd16c..9581ce872a288 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/get_es_deprecation_error.ts @@ -6,13 +6,13 @@ */ import { i18n } from '@kbn/i18n'; -import { ResponseError } from './api'; +import { ResponseError } from '../../../common/types'; const i18nTexts = { permissionsError: i18n.translate( 'xpack.upgradeAssistant.esDeprecationErrors.permissionsErrorMessage', { - defaultMessage: 'You are not authorized to view Elasticsearch deprecations.', + defaultMessage: 'You are not authorized to view Elasticsearch deprecation issues.', } ), partiallyUpgradedWarning: i18n.translate( @@ -29,7 +29,7 @@ const i18nTexts = { } ), loadingError: i18n.translate('xpack.upgradeAssistant.esDeprecationErrors.loadingErrorMessage', { - defaultMessage: 'Could not retrieve Elasticsearch deprecations.', + defaultMessage: 'Could not retrieve Elasticsearch deprecation issues.', }), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts new file mode 100644 index 0000000000000..59c3adaed95df --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.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 moment from 'moment-timezone'; + +import { Storage } from '../../shared_imports'; + +const SETTING_ID = 'kibana.upgradeAssistant.lastCheckpoint'; +const localStorage = new Storage(window.localStorage); + +export const loadLogsCheckpoint = () => { + const storedValue = moment(localStorage.get(SETTING_ID)); + + if (storedValue.isValid()) { + return storedValue.toISOString(); + } + + const now = moment().toISOString(); + localStorage.set(SETTING_ID, now); + + return now; +}; + +export const saveLogsCheckpoint = (value: string) => { + localStorage.set(SETTING_ID, value); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts new file mode 100644 index 0000000000000..394f046a8bafe --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/ui_metric.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UiCounterMetricType } from '@kbn/analytics'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +export const UIM_APP_NAME = 'upgrade_assistant'; +export const UIM_ES_DEPRECATIONS_PAGE_LOAD = 'es_deprecations_page_load'; +export const UIM_KIBANA_DEPRECATIONS_PAGE_LOAD = 'kibana_deprecations_page_load'; +export const UIM_OVERVIEW_PAGE_LOAD = 'overview_page_load'; +export const UIM_REINDEX_OPEN_FLYOUT_CLICK = 'reindex_open_flyout_click'; +export const UIM_REINDEX_CLOSE_FLYOUT_CLICK = 'reindex_close_flyout_click'; +export const UIM_REINDEX_START_CLICK = 'reindex_start_click'; +export const UIM_REINDEX_STOP_CLICK = 'reindex_stop_click'; +export const UIM_BACKUP_DATA_CLOUD_CLICK = 'backup_data_cloud_click'; +export const UIM_BACKUP_DATA_ON_PREM_CLICK = 'backup_data_on_prem_click'; +export const UIM_RESET_LOGS_COUNTER_CLICK = 'reset_logs_counter_click'; +export const UIM_OBSERVABILITY_CLICK = 'observability_click'; +export const UIM_DISCOVER_CLICK = 'discover_click'; +export const UIM_ML_SNAPSHOT_UPGRADE_CLICK = 'ml_snapshot_upgrade_click'; +export const UIM_ML_SNAPSHOT_DELETE_CLICK = 'ml_snapshot_delete_click'; +export const UIM_INDEX_SETTINGS_DELETE_CLICK = 'index_settings_delete_click'; +export const UIM_KIBANA_QUICK_RESOLVE_CLICK = 'kibana_quick_resolve_click'; + +export class UiMetricService { + private usageCollection: UsageCollectionSetup | undefined; + + public setup(usageCollection: UsageCollectionSetup) { + this.usageCollection = usageCollection; + } + + private track(metricType: UiCounterMetricType, eventName: string | string[]) { + if (!this.usageCollection) { + // Usage collection might be disabled in Kibana config. + return; + } + return this.usageCollection.reportUiCounter(UIM_APP_NAME, metricType, eventName); + } + + public trackUiMetric(metricType: UiCounterMetricType, eventName: string | string[]) { + return this.track(metricType, eventName); + } +} + +export const uiMetricService = new UiMetricService(); diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/utils.test.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/utils.test.ts index 83fc9cabbbecc..37392c832ecf5 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/utils.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/utils.test.ts @@ -6,7 +6,8 @@ */ import { DEPRECATION_WARNING_UPPER_LIMIT } from '../../../common/constants'; -import { validateRegExpString, getDeprecationsUpperLimit } from './utils'; +import { getDeprecationsUpperLimit, getReindexProgressLabel, validateRegExpString } from './utils'; +import { ReindexStep } from '../../../common/types'; describe('validRegExpString', () => { it('correctly returns false for invalid strings', () => { @@ -35,3 +36,33 @@ describe('getDeprecationsUpperLimit', () => { ); }); }); + +describe('getReindexProgressLabel', () => { + it('returns 0% when the reindex task has just been created', () => { + expect(getReindexProgressLabel(null, ReindexStep.created)).toBe('0%'); + }); + + it('returns 5% when the index has been made read-only', () => { + expect(getReindexProgressLabel(null, ReindexStep.readonly)).toBe('5%'); + }); + + it('returns 10% when the reindexing documents has started, but the progress is null', () => { + expect(getReindexProgressLabel(null, ReindexStep.reindexStarted)).toBe('10%'); + }); + + it('returns 10% when the reindexing documents has started, but the progress is 0', () => { + expect(getReindexProgressLabel(0, ReindexStep.reindexStarted)).toBe('10%'); + }); + + it('returns 53% when the reindexing documents progress is 0.5', () => { + expect(getReindexProgressLabel(0.5, ReindexStep.reindexStarted)).toBe('53%'); + }); + + it('returns 95% when the reindexing documents progress is 1', () => { + expect(getReindexProgressLabel(1, ReindexStep.reindexStarted)).toBe('95%'); + }); + + it('returns 100% when alias has been switched', () => { + expect(getReindexProgressLabel(null, ReindexStep.aliasCreated)).toBe('100%'); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/utils.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/utils.ts index b90038e1166ab..bdbc0949e368b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/lib/utils.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/utils.ts @@ -9,6 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { tryCatch, fold } from 'fp-ts/lib/Either'; import { DEPRECATION_WARNING_UPPER_LIMIT } from '../../../common/constants'; +import { ReindexStep } from '../../../common/types'; export const validateRegExpString = (s: string) => pipe( @@ -34,3 +35,50 @@ export const getDeprecationsUpperLimit = (count: number) => { return count.toString(); }; + +/* + * Reindexing task consists of 4 steps: making the index read-only, creating a new index, + * reindexing documents into the new index and switching alias from the old to the new index. + * Steps 1, 2 and 4 each contribute 5% to the overall progress. + * Step 3 (reindexing documents) can take a long time for large indices and its progress is calculated + * between 10% and 95% of the overall progress depending on its completeness percentage. + */ +export const getReindexProgressLabel = ( + reindexTaskPercComplete: number | null, + lastCompletedStep: ReindexStep | undefined +): string => { + let percentsComplete = 0; + switch (lastCompletedStep) { + case ReindexStep.created: + // the reindex task has just started, 0% progress + percentsComplete = 0; + break; + case ReindexStep.readonly: { + // step 1 completed, 5% progress + percentsComplete = 5; + break; + } + case ReindexStep.newIndexCreated: { + // step 2 completed, 10% progress + percentsComplete = 10; + break; + } + case ReindexStep.reindexStarted: { + // step 3 started, 10-95% progress depending on progress of reindexing documents in ES + percentsComplete = + reindexTaskPercComplete !== null ? 10 + Math.round(reindexTaskPercComplete * 85) : 10; + break; + } + case ReindexStep.reindexCompleted: { + // step 3 completed, only step 4 remaining, 95% progress + percentsComplete = 95; + break; + } + case ReindexStep.aliasCreated: { + // step 4 completed, 100% progress + percentsComplete = 100; + break; + } + } + return `${percentsComplete}%`; +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.ts deleted file mode 100644 index 7d6d071fcf95f..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.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 { CoreSetup } from 'src/core/public'; -import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; -import { renderApp } from './render_app'; -import { KibanaVersionContext } from './app_context'; -import { apiService } from './lib/api'; -import { breadcrumbService } from './lib/breadcrumbs'; -import { AppServicesContext } from '../types'; - -export async function mountManagementSection( - coreSetup: CoreSetup, - params: ManagementAppMountParams, - kibanaVersionInfo: KibanaVersionContext, - readonly: boolean, - services: AppServicesContext -) { - const [{ i18n, docLinks, notifications, application, deprecations }] = - await coreSetup.getStartServices(); - - const { element, history, setBreadcrumbs } = params; - const { http } = coreSetup; - - apiService.setup(http); - breadcrumbService.setup(setBreadcrumbs); - - return renderApp({ - element, - http, - i18n, - docLinks, - kibanaVersionInfo, - notifications, - isReadOnlyMode: readonly, - history, - api: apiService, - breadcrumbs: breadcrumbService, - getUrlForApp: application.getUrlForApp, - deprecations, - application, - services, - }); -} diff --git a/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.tsx b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.tsx new file mode 100644 index 0000000000000..6ab764ddcba6d --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/mount_management_section.tsx @@ -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 React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import type { ManagementAppMountParams } from 'src/plugins/management/public'; +import { RootComponent } from './app'; +import { AppDependencies } from '../types'; + +import { apiService } from './lib/api'; +import { breadcrumbService } from './lib/breadcrumbs'; + +export function mountManagementSection( + params: ManagementAppMountParams, + dependencies: AppDependencies +) { + const { element, setBreadcrumbs } = params; + + apiService.setup(dependencies.services.core.http); + breadcrumbService.setup(setBreadcrumbs); + + render(, element); + + return () => { + unmountComponentAtNode(element); + }; +} diff --git a/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx b/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx deleted file mode 100644 index 248e6961a74e5..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/application/render_app.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { AppDependencies, RootComponent } from './app'; - -interface BootDependencies extends AppDependencies { - element: HTMLElement; -} - -export const renderApp = (deps: BootDependencies) => { - const { element, ...appDependencies } = deps; - render(, element); - return () => { - unmountComponentAtNode(element); - }; -}; diff --git a/x-pack/plugins/upgrade_assistant/public/index.scss b/x-pack/plugins/upgrade_assistant/public/index.scss deleted file mode 100644 index 9bd47b6473372..0000000000000 --- a/x-pack/plugins/upgrade_assistant/public/index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './application/index'; diff --git a/x-pack/plugins/upgrade_assistant/public/index.ts b/x-pack/plugins/upgrade_assistant/public/index.ts index a4091bcb3e1ab..e338b9c044f68 100644 --- a/x-pack/plugins/upgrade_assistant/public/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import './index.scss'; import { PluginInitializerContext } from 'src/core/public'; import { UpgradeAssistantUIPlugin } from './plugin'; diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index 32e825fbdc20d..2b0ad7241b3af 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -9,19 +9,20 @@ import SemVer from 'semver/classes/semver'; import { i18n } from '@kbn/i18n'; import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; -import { - SetupDependencies, - StartDependencies, - AppServicesContext, - ClientConfigType, -} from './types'; +import { apiService } from './application/lib/api'; +import { breadcrumbService } from './application/lib/breadcrumbs'; +import { uiMetricService } from './application/lib/ui_metric'; +import { SetupDependencies, StartDependencies, AppDependencies, ClientConfigType } from './types'; export class UpgradeAssistantUIPlugin implements Plugin { constructor(private ctx: PluginInitializerContext) {} - setup(coreSetup: CoreSetup, { management, cloud }: SetupDependencies) { + setup( + coreSetup: CoreSetup, + { management, cloud, share, usageCollection }: SetupDependencies + ) { const { readonly, ui: { enabled: isUpgradeAssistantUiEnabled }, @@ -38,17 +39,19 @@ export class UpgradeAssistantUIPlugin }; const pluginName = i18n.translate('xpack.upgradeAssistant.appTitle', { - defaultMessage: '{version} Upgrade Assistant', - values: { version: `${kibanaVersionInfo.nextMajor}.0` }, + defaultMessage: 'Upgrade Assistant', }); + if (usageCollection) { + uiMetricService.setup(usageCollection); + } + appRegistrar.registerApp({ id: 'upgrade_assistant', title: pluginName, order: 1, async mount(params) { - const [coreStart, { discover, data }] = await coreSetup.getStartServices(); - const services: AppServicesContext = { discover, data, cloud }; + const [coreStart, { data, ...plugins }] = await coreSetup.getStartServices(); const { chrome: { docTitle }, @@ -56,14 +59,28 @@ export class UpgradeAssistantUIPlugin docTitle.change(pluginName); - const { mountManagementSection } = await import('./application/mount_management_section'); - const unmountAppCallback = await mountManagementSection( - coreSetup, - params, + const appDependencies: AppDependencies = { kibanaVersionInfo, - readonly, - services - ); + isReadOnlyMode: readonly, + plugins: { + cloud, + share, + // Infra plugin doesnt export anything as a public interface. So the only + // way we have at this stage for checking if the plugin is available or not + // is by checking if the startServices has the `infra` key. + infra: plugins.hasOwnProperty('infra') ? {} : undefined, + }, + services: { + core: coreStart, + data, + history: params.history, + api: apiService, + breadcrumbs: breadcrumbService, + }, + }; + + const { mountManagementSection } = await import('./application/mount_management_section'); + const unmountAppCallback = mountManagementSection(params, appDependencies); return () => { docTitle.reset(); diff --git a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts index 06816daac428b..c6c00f34bfadf 100644 --- a/x-pack/plugins/upgrade_assistant/public/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/public/shared_imports.ts @@ -5,23 +5,31 @@ * 2.0. */ -import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; -import { AppServicesContext } from './types'; - export type { SendRequestConfig, SendRequestResponse, UseRequestConfig, + Privileges, + MissingPrivileges, + Authorization, } from '../../../../src/plugins/es_ui_shared/public/'; export { sendRequest, useRequest, SectionLoading, GlobalFlyout, + WithPrivileges, + AuthorizationProvider, + AuthorizationContext, } from '../../../../src/plugins/es_ui_shared/public/'; -export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +export { Storage } from '../../../../src/plugins/kibana_utils/public'; + +export { + KibanaContextProvider, + reactRouterNavigate, +} from '../../../../src/plugins/kibana_react/public'; export type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -export const useKibana = () => _useKibana(); +export { APP_WRAPPER_CLASS } from '../../../../src/core/public'; diff --git a/x-pack/plugins/upgrade_assistant/public/types.ts b/x-pack/plugins/upgrade_assistant/public/types.ts index cbeaf22bb095b..ace009d9c74aa 100644 --- a/x-pack/plugins/upgrade_assistant/public/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/types.ts @@ -5,25 +5,32 @@ * 2.0. */ -import { DiscoverStart } from 'src/plugins/discover/public'; +import { ScopedHistory } from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { SharePluginSetup } from 'src/plugins/share/public'; +import { CoreStart } from 'src/core/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; +import { BreadcrumbService } from './application/lib/breadcrumbs'; +import { ApiService } from './application/lib/api'; -export interface AppServicesContext { - cloud?: CloudSetup; - discover: DiscoverStart; - data: DataPublicPluginStart; +export interface KibanaVersionContext { + currentMajor: number; + prevMajor: number; + nextMajor: number; } export interface SetupDependencies { management: ManagementSetup; + share: SharePluginSetup; cloud?: CloudSetup; + usageCollection?: UsageCollectionSetup; } + export interface StartDependencies { licensing: LicensingPluginStart; - discover: DiscoverStart; data: DataPublicPluginStart; } @@ -33,3 +40,20 @@ export interface ClientConfigType { enabled: boolean; }; } + +export interface AppDependencies { + isReadOnlyMode: boolean; + kibanaVersionInfo: KibanaVersionContext; + plugins: { + cloud?: CloudSetup; + share: SharePluginSetup; + infra: object | undefined; + }; + services: { + core: CoreStart; + data: DataPublicPluginStart; + breadcrumbs: BreadcrumbService; + history: ScopedHistory; + api: ApiService; + }; +} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json index 617bb02ff9dfc..2337e0e2dc039 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json +++ b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/fake_deprecations.json @@ -102,6 +102,15 @@ "resolve_during_rolling_upgrade": false } ], + ".ml-config": [ + { + "level": "critical", + "message": "Index created before 7.0", + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields", + "details": "This index was created using version: 6.8.16", + "resolve_during_rolling_upgrade": false + } + ], ".watcher-history-6-2018.11.07": [ { "level": "warning", diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts index 0e01d8d6a3458..b3b93582e2260 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts @@ -64,7 +64,7 @@ describe('setDeprecationLogging', () => { }); describe('isDeprecationLoggingEnabled', () => { - ['default', 'persistent', 'transient'].forEach((tier) => { + ['defaults', 'persistent', 'transient'].forEach((tier) => { ['ALL', 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ALL'].forEach((level) => { it(`returns true when ${tier} is set to ${level}`, () => { expect(isDeprecationLoggingEnabled({ [tier]: { logger: { deprecation: level } } })).toBe( @@ -74,7 +74,7 @@ describe('isDeprecationLoggingEnabled', () => { }); }); - ['default', 'persistent', 'transient'].forEach((tier) => { + ['defaults', 'persistent', 'transient'].forEach((tier) => { ['ERROR', 'FATAL'].forEach((level) => { it(`returns false when ${tier} is set to ${level}`, () => { expect(isDeprecationLoggingEnabled({ [tier]: { logger: { deprecation: level } } })).toBe( @@ -87,7 +87,7 @@ describe('isDeprecationLoggingEnabled', () => { it('allows transient to override persistent and default', () => { expect( isDeprecationLoggingEnabled({ - default: { logger: { deprecation: 'FATAL' } }, + defaults: { logger: { deprecation: 'FATAL' } }, persistent: { logger: { deprecation: 'FATAL' } }, transient: { logger: { deprecation: 'WARN' } }, }) @@ -97,7 +97,7 @@ describe('isDeprecationLoggingEnabled', () => { it('allows persistent to override default', () => { expect( isDeprecationLoggingEnabled({ - default: { logger: { deprecation: 'FATAL' } }, + defaults: { logger: { deprecation: 'FATAL' } }, persistent: { logger: { deprecation: 'WARN' } }, }) ).toBe(true); @@ -108,7 +108,7 @@ describe('isDeprecationLogIndexingEnabled', () => { it('allows transient to override persistent and default', () => { expect( isDeprecationLogIndexingEnabled({ - default: { cluster: { deprecation_indexing: { enabled: 'false' } } }, + defaults: { cluster: { deprecation_indexing: { enabled: 'false' } } }, persistent: { cluster: { deprecation_indexing: { enabled: 'false' } } }, transient: { cluster: { deprecation_indexing: { enabled: 'true' } } }, }) @@ -118,7 +118,7 @@ describe('isDeprecationLogIndexingEnabled', () => { it('allows persistent to override default', () => { expect( isDeprecationLogIndexingEnabled({ - default: { cluster: { deprecation_indexing: { enabled: 'false' } } }, + defaults: { cluster: { deprecation_indexing: { enabled: 'false' } } }, persistent: { cluster: { deprecation_indexing: { enabled: 'true' } } }, }) ).toBe(true); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts index 214aabb989921..2793c2c6ac818 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.ts @@ -51,7 +51,7 @@ export async function setDeprecationLogging( } export function isDeprecationLogIndexingEnabled(settings: any) { - const clusterDeprecationLoggingEnabled = ['default', 'persistent', 'transient'].reduce( + const clusterDeprecationLoggingEnabled = ['defaults', 'persistent', 'transient'].reduce( (currentLogLevel, settingsTier) => get(settings, [settingsTier, 'cluster', 'deprecation_indexing', 'enabled'], currentLogLevel), 'false' @@ -61,7 +61,7 @@ export function isDeprecationLogIndexingEnabled(settings: any) { } export function isDeprecationLoggingEnabled(settings: any) { - const deprecationLogLevel = ['default', 'persistent', 'transient'].reduce( + const deprecationLogLevel = ['defaults', 'persistent', 'transient'].reduce( (currentLogLevel, settingsTier) => get(settings, [settingsTier, 'logger', 'deprecation'], currentLogLevel), 'WARN' diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.test.ts index 99c101e04e36b..06c0352ebcdca 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.test.ts @@ -40,6 +40,25 @@ describe('getESUpgradeStatus', () => { asApiResponse(deprecationsResponse) ); + esClient.asCurrentUser.transport.request.mockResolvedValue( + asApiResponse({ + features: [ + { + feature_name: 'machine_learning', + minimum_index_version: '7.1.1', + migration_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.ml-config', + version: '7.1.1', + }, + ], + }, + ], + migration_status: 'MIGRATION_NEEDED', + }) + ); + // @ts-expect-error not full interface of response esClient.asCurrentUser.indices.resolveIndex.mockResolvedValue(asApiResponse(resolvedIndices)); @@ -86,4 +105,30 @@ describe('getESUpgradeStatus', () => { 0 ); }); + + it('filters out system indices returned by upgrade system indices API', async () => { + esClient.asCurrentUser.migration.deprecations.mockResolvedValue( + asApiResponse({ + cluster_settings: [], + node_settings: [], + ml_settings: [], + index_settings: { + '.ml-config': [ + { + level: 'critical', + message: 'Index created before 7.0', + url: 'https://', + details: '...', + resolve_during_rolling_upgrade: false, + }, + ], + }, + }) + ); + + const upgradeStatus = await getESUpgradeStatus(esClient); + + expect(upgradeStatus.deprecations).toHaveLength(0); + expect(upgradeStatus.totalCriticalDeprecations).toBe(0); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts index aa08ecef78d32..2e2c80b790cd5 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecations_status.ts @@ -11,6 +11,10 @@ import { indexSettingDeprecations } from '../../common/constants'; import { EnrichedDeprecationInfo, ESUpgradeStatus } from '../../common/types'; import { esIndicesStateCheck } from './es_indices_state_check'; +import { + getESSystemIndicesMigrationStatus, + convertFeaturesToIndicesArray, +} from '../lib/es_system_indices_migration'; export async function getESUpgradeStatus( dataClient: IScopedClusterClient @@ -19,10 +23,19 @@ export async function getESUpgradeStatus( const getCombinedDeprecations = async () => { const indices = await getCombinedIndexInfos(deprecations, dataClient); + const systemIndices = await getESSystemIndicesMigrationStatus(dataClient.asCurrentUser); + const systemIndicesList = convertFeaturesToIndicesArray(systemIndices.features); return Object.keys(deprecations).reduce((combinedDeprecations, deprecationType) => { if (deprecationType === 'index_settings') { - combinedDeprecations = combinedDeprecations.concat(indices); + // We need to exclude all index related deprecations for system indices since + // they are resolved separately through the system indices upgrade section in + // the Overview page. + const withoutSystemIndices = indices.filter( + (index) => !systemIndicesList.includes(index.index!) + ); + + combinedDeprecations = combinedDeprecations.concat(withoutSystemIndices); } else { const deprecationsByType = deprecations[ deprecationType as keyof estypes.MigrationDeprecationsResponse diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts new file mode 100644 index 0000000000000..560d42712b5da --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { convertFeaturesToIndicesArray } from './es_system_indices_migration'; +import { SystemIndicesMigrationStatus } from '../../common/types'; + +const esUpgradeSystemIndicesStatusMock: SystemIndicesMigrationStatus = { + features: [ + { + feature_name: 'machine_learning', + minimum_index_version: '7.1.1', + migration_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.ml-config', + version: '7.1.1', + }, + { + index: '.ml-notifications', + version: '7.1.1', + }, + ], + }, + { + feature_name: 'security', + minimum_index_version: '7.1.1', + migration_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.ml-config', + version: '7.1.1', + }, + ], + }, + ], + migration_status: 'MIGRATION_NEEDED', +}; + +describe('convertFeaturesToIndicesArray', () => { + it('converts list with features to flat array of uniq indices', async () => { + const result = convertFeaturesToIndicesArray(esUpgradeSystemIndicesStatusMock.features); + expect(result).toEqual(['.ml-config', '.ml-notifications']); + }); + + it('returns empty array if no features are passed to it', async () => { + expect(convertFeaturesToIndicesArray([])).toEqual([]); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts new file mode 100644 index 0000000000000..aa239de7dd008 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_system_indices_migration.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { flow, flatMap, map, flatten, uniq } from 'lodash/fp'; +import { ElasticsearchClient } from 'src/core/server'; +import { + SystemIndicesMigrationStatus, + SystemIndicesMigrationFeature, + SystemIndicesMigrationStarted, +} from '../../common/types'; + +export const convertFeaturesToIndicesArray = ( + features: SystemIndicesMigrationFeature[] +): string[] => { + return flow( + // Map each feature into Indices[] + map('indices'), + // Flatten each into an string[] of indices + map(flatMap('index')), + // Flatten the array + flatten, + // And finally dedupe the indices + uniq + )(features); +}; + +export const getESSystemIndicesMigrationStatus = async ( + client: ElasticsearchClient +): Promise => { + const { body } = await client.transport.request({ + method: 'GET', + path: '/_migration/system_features', + }); + + return body as SystemIndicesMigrationStatus; +}; + +export const startESSystemIndicesMigration = async ( + client: ElasticsearchClient +): Promise => { + const { body } = await client.transport.request({ + method: 'POST', + path: '/_migration/system_features', + }); + + return body as SystemIndicesMigrationStarted; +}; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts index 8bf9143d93dbc..8532e2e4eece4 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.test.ts @@ -5,33 +5,171 @@ * 2.0. */ +import { KibanaRequest } from 'src/core/server'; +import { loggingSystemMock, httpServerMock } from 'src/core/server/mocks'; +import { securityMock } from '../../../../security/server/mocks'; import { ReindexSavedObject } from '../../../common/types'; -import { Credential, credentialStoreFactory } from './credential_store'; +import { credentialStoreFactory } from './credential_store'; + +const basicAuthHeader = 'Basic abc'; + +const logMock = loggingSystemMock.create().get(); +const requestMock = KibanaRequest.from( + httpServerMock.createRawRequest({ + headers: { + authorization: basicAuthHeader, + }, + }) +); +const securityStartMock = securityMock.createStart(); + +const reindexOpMock = { + id: 'asdf', + attributes: { indexName: 'test', lastCompletedStep: 1, locked: null }, +} as ReindexSavedObject; describe('credentialStore', () => { - it('retrieves the same credentials for the same state', () => { - const creds = { key: '1' } as Credential; - const reindexOp = { - id: 'asdf', - attributes: { indexName: 'test', lastCompletedStep: 1, locked: null }, - } as ReindexSavedObject; - - const credStore = credentialStoreFactory(); - credStore.set(reindexOp, creds); - expect(credStore.get(reindexOp)).toEqual(creds); + it('retrieves the same credentials for the same state', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + expect(credStore.get(reindexOpMock)).toEqual({ + authorization: basicAuthHeader, + }); + }); + + it('does not retrieve credentials if the state changed', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + reindexOpMock.attributes.lastCompletedStep = 0; + + expect(credStore.get(reindexOpMock)).toBeUndefined(); + }); + + it('retrieves credentials after update', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + const updatedReindexOp = { + ...reindexOpMock, + attributes: { + ...reindexOpMock.attributes, + status: 0, + }, + }; + + await credStore.update({ + credential: { + authorization: basicAuthHeader, + }, + reindexOp: updatedReindexOp, + security: securityStartMock, + }); + + expect(credStore.get(updatedReindexOp)).toEqual({ + authorization: basicAuthHeader, + }); }); - it('does retrieve credentials if the state is changed', () => { - const creds = { key: '1' } as Credential; - const reindexOp = { - id: 'asdf', - attributes: { indexName: 'test', lastCompletedStep: 1, locked: null }, - } as ReindexSavedObject; + describe('API keys enabled', () => { + const apiKeyResultMock = { + id: 'api_key_id', + name: 'api_key_name', + api_key: '123', + }; + + const invalidateApiKeyResultMock = { + invalidated_api_keys: [apiKeyResultMock.api_key], + previously_invalidated_api_keys: [], + error_count: 0, + }; + + const base64ApiKey = Buffer.from(`${apiKeyResultMock.id}:${apiKeyResultMock.api_key}`).toString( + 'base64' + ); + + beforeEach(() => { + securityStartMock.authc.apiKeys.areAPIKeysEnabled.mockReturnValue(Promise.resolve(true)); + securityStartMock.authc.apiKeys.grantAsInternalUser.mockReturnValue( + Promise.resolve(apiKeyResultMock) + ); + securityStartMock.authc.apiKeys.invalidateAsInternalUser.mockReturnValue( + Promise.resolve(invalidateApiKeyResultMock) + ); + }); + + it('sets API key in authorization header', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + expect(credStore.get(reindexOpMock)).toEqual({ + authorization: `ApiKey ${base64ApiKey}`, + }); + }); + + it('invalidates API keys when a reindex operation is complete', async () => { + const credStore = credentialStoreFactory(logMock); + + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); + + await credStore.update({ + credential: { + authorization: `ApiKey ${base64ApiKey}`, + }, + reindexOp: { + ...reindexOpMock, + attributes: { + ...reindexOpMock.attributes, + status: 1, + }, + }, + security: securityStartMock, + }); + + expect(securityStartMock.authc.apiKeys.invalidateAsInternalUser).toHaveBeenCalled(); + }); + + it('falls back to user credentials when error granting API key', async () => { + const credStore = credentialStoreFactory(logMock); + + securityStartMock.authc.apiKeys.grantAsInternalUser.mockRejectedValue( + new Error('Error granting API key') + ); - const credStore = credentialStoreFactory(); - credStore.set(reindexOp, creds); + await credStore.set({ + request: requestMock, + reindexOp: reindexOpMock, + security: securityStartMock, + }); - reindexOp.attributes.lastCompletedStep = 0; - expect(credStore.get(reindexOp)).not.toBeDefined(); + expect(credStore.get(reindexOpMock)).toEqual({ + authorization: basicAuthHeader, + }); + }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts index 2c4f86824518a..66885a23cf96b 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts @@ -8,10 +8,73 @@ import { createHash } from 'crypto'; import stringify from 'json-stable-stringify'; -import { ReindexSavedObject } from '../../../common/types'; +import { KibanaRequest, Logger } from 'src/core/server'; + +import { SecurityPluginStart } from '../../../../security/server'; +import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; export type Credential = Record; +// Generates a stable hash for the reindex operation's current state. +const getHash = (reindexOp: ReindexSavedObject) => + createHash('sha256') + .update(stringify({ id: reindexOp.id, ...reindexOp.attributes })) + .digest('base64'); + +// Returns a base64-encoded API key string or undefined +const getApiKey = async ({ + request, + security, + reindexOpId, + apiKeysMap, +}: { + request: KibanaRequest; + security: SecurityPluginStart; + reindexOpId: string; + apiKeysMap: Map; +}): Promise => { + try { + const apiKeyResult = await security.authc.apiKeys.grantAsInternalUser(request, { + name: `ua_reindex_${reindexOpId}`, + role_descriptors: {}, + metadata: { + description: + 'Created by the Upgrade Assistant for a reindex operation; this can be safely deleted after Kibana is upgraded.', + }, + }); + + if (apiKeyResult) { + const { api_key: apiKey, id } = apiKeyResult; + // Store each API key per reindex operation so that we can later invalidate it when the reindex operation is complete + apiKeysMap.set(reindexOpId, id); + // Returns the base64 encoding of `id:api_key` + // This can be used when sending a request with an "Authorization: ApiKey xxx" header + return Buffer.from(`${id}:${apiKey}`).toString('base64'); + } + } catch (error) { + // There are a few edge cases were granting an API key could fail, + // in which case we fall back to using the requestor's credentials in memory + return undefined; + } +}; + +const invalidateApiKey = async ({ + apiKeyId, + security, + log, +}: { + apiKeyId: string; + security?: SecurityPluginStart; + log: Logger; +}) => { + try { + await security?.authc.apiKeys.invalidateAsInternalUser({ ids: [apiKeyId] }); + } catch (error) { + // Swallow error if there's a problem invalidating API key + log.debug(`Error invalidating API key for id ${apiKeyId}: ${error.message}`); + } +}; + /** * An in-memory cache for user credentials to be used for reindexing operations. When looking up * credentials, the reindex operation must be in the same state it was in when the credentials @@ -20,25 +83,82 @@ export type Credential = Record; */ export interface CredentialStore { get(reindexOp: ReindexSavedObject): Credential | undefined; - set(reindexOp: ReindexSavedObject, credential: Credential): void; + set(params: { + reindexOp: ReindexSavedObject; + request: KibanaRequest; + security?: SecurityPluginStart; + }): Promise; + update(params: { + reindexOp: ReindexSavedObject; + security?: SecurityPluginStart; + credential: Credential; + }): Promise; clear(): void; } -export const credentialStoreFactory = (): CredentialStore => { +export const credentialStoreFactory = (logger: Logger): CredentialStore => { const credMap = new Map(); - - // Generates a stable hash for the reindex operation's current state. - const getHash = (reindexOp: ReindexSavedObject) => - createHash('sha256') - .update(stringify({ id: reindexOp.id, ...reindexOp.attributes })) - .digest('base64'); + const apiKeysMap = new Map(); + const log = logger.get('credential_store'); return { get(reindexOp: ReindexSavedObject) { return credMap.get(getHash(reindexOp)); }, - set(reindexOp: ReindexSavedObject, credential: Credential) { + async set({ + reindexOp, + request, + security, + }: { + reindexOp: ReindexSavedObject; + request: KibanaRequest; + security?: SecurityPluginStart; + }) { + const areApiKeysEnabled = (await security?.authc.apiKeys.areAPIKeysEnabled()) ?? false; + + if (areApiKeysEnabled) { + const apiKey = await getApiKey({ + request, + security: security!, + reindexOpId: reindexOp.id, + apiKeysMap, + }); + + if (apiKey) { + credMap.set(getHash(reindexOp), { + ...request.headers, + authorization: `ApiKey ${apiKey}`, + }); + return; + } + } + + // Set the requestor's credentials in memory if apiKeys are not enabled + credMap.set(getHash(reindexOp), request.headers); + }, + + async update({ + reindexOp, + security, + credential, + }: { + reindexOp: ReindexSavedObject; + security?: SecurityPluginStart; + credential: Credential; + }) { + // If the reindex operation is completed... + if (reindexOp.attributes.status === ReindexStatus.completed) { + // ...and an API key is being used, invalidate it + const apiKeyId = apiKeysMap.get(reindexOp.id); + if (apiKeyId) { + await invalidateApiKey({ apiKeyId, security, log }); + apiKeysMap.delete(reindexOp.id); + return; + } + } + + // Otherwise, re-associate the credentials credMap.set(getHash(reindexOp), credential); }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 7595e1da7b573..3f58a04949da5 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -13,7 +13,6 @@ import { ScopedClusterClientMock } from 'src/core/server/elasticsearch/client/mo import moment from 'moment'; import { - IndexGroup, REINDEX_OP_TYPE, ReindexSavedObject, ReindexStatus, @@ -283,46 +282,4 @@ describe('ReindexActions', () => { await expect(actions.getFlatSettings('myIndex')).resolves.toBeNull(); }); }); - - describe('runWhileConsumerLocked', () => { - Object.entries(IndexGroup).forEach(([typeKey, consumerType]) => { - describe(`IndexConsumerType.${typeKey}`, () => { - it('creates the lock doc if it does not exist and executes callback', async () => { - expect.assertions(3); - client.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError()); // mock no ML doc exists yet - client.create.mockImplementationOnce((type: any, attributes: any, { id }: any) => - Promise.resolve({ - type, - id, - attributes, - }) - ); - - let flip = false; - await actions.runWhileIndexGroupLocked(consumerType, async (mlDoc) => { - expect(mlDoc.id).toEqual(consumerType); - expect(mlDoc.attributes.runningReindexCount).toEqual(0); - flip = true; - return mlDoc; - }); - expect(flip).toEqual(true); - }); - - it('fails after 10 attempts to lock', async () => { - client.get.mockResolvedValue({ - type: REINDEX_OP_TYPE, - id: consumerType, - attributes: { mlReindexCount: 0 }, - }); - - client.update.mockRejectedValue(new Error('NO LOCKING!')); - - await expect( - actions.runWhileIndexGroupLocked(consumerType, async (m) => m) - ).rejects.toThrow('Could not acquire lock for ML jobs'); - expect(client.update).toHaveBeenCalledTimes(10); - }, 20000); - }); - }); - }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts index fe8844b28e37a..09ba4b744e68e 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts @@ -13,7 +13,6 @@ import { ElasticsearchClient, } from 'src/core/server'; import { - IndexGroup, REINDEX_OP_TYPE, ReindexOperation, ReindexOptions, @@ -33,11 +32,6 @@ export const LOCK_WINDOW = moment.duration(90, 'seconds'); * This is NOT intended to be used by any other code. */ export interface ReindexActions { - /** - * Namespace for ML-specific actions. - */ - // ml: MlActions; - /** * Creates a new reindexOp, does not perform any pre-flight checks. * @param indexName @@ -86,34 +80,10 @@ export interface ReindexActions { * Retrieve index settings (in flat, dot-notation style) and mappings. * @param indexName */ - getFlatSettings(indexName: string): Promise; - - // ----- Functions below are for enforcing locks around groups of indices like ML or Watcher - - /** - * Atomically increments the number of reindex operations running for an index group. - */ - incrementIndexGroupReindexes(group: IndexGroup): Promise; - - /** - * Atomically decrements the number of reindex operations running for an index group. - */ - decrementIndexGroupReindexes(group: IndexGroup): Promise; - - /** - * Runs a callback function while locking an index group. - * @param func A function to run with the locked index group lock document. Must return a promise that resolves - * to the updated ReindexSavedObject. - */ - runWhileIndexGroupLocked( - group: IndexGroup, - func: (lockDoc: ReindexSavedObject) => Promise - ): Promise; - - /** - * Exposed only for testing, DO NOT USE. - */ - _fetchAndLockIndexGroupDoc(group: IndexGroup): Promise; + getFlatSettings( + indexName: string, + withTypeName?: boolean + ): Promise; } export const reindexActionsFactory = ( @@ -266,76 +236,5 @@ export const reindexActionsFactory = ( return flatSettings.body[indexName]; }, - - async _fetchAndLockIndexGroupDoc(indexGroup) { - const fetchDoc = async () => { - try { - // The IndexGroup enum value (a string) serves as the ID of the lock doc - return await client.get(REINDEX_OP_TYPE, indexGroup); - } catch (e) { - if (client.errors.isNotFoundError(e)) { - return await client.create( - REINDEX_OP_TYPE, - { - indexName: null, - newIndexName: null, - locked: null, - status: null, - lastCompletedStep: null, - reindexTaskId: null, - reindexTaskPercComplete: null, - errorMessage: null, - runningReindexCount: 0, - } as any, - { id: indexGroup } - ); - } else { - throw e; - } - } - }; - - const lockDoc = async (attempt = 1): Promise => { - try { - // Refetch the document each time to avoid version conflicts. - return await acquireLock(await fetchDoc()); - } catch (e) { - if (attempt >= 10) { - throw new Error(`Could not acquire lock for ML jobs`); - } - - await new Promise((resolve) => setTimeout(resolve, 1000)); - return lockDoc(attempt + 1); - } - }; - - return lockDoc(); - }, - - async incrementIndexGroupReindexes(indexGroup) { - this.runWhileIndexGroupLocked(indexGroup, (lockDoc) => - this.updateReindexOp(lockDoc, { - runningReindexCount: lockDoc.attributes.runningReindexCount! + 1, - }) - ); - }, - - async decrementIndexGroupReindexes(indexGroup) { - this.runWhileIndexGroupLocked(indexGroup, (lockDoc) => - this.updateReindexOp(lockDoc, { - runningReindexCount: lockDoc.attributes.runningReindexCount! - 1, - }) - ); - }, - - async runWhileIndexGroupLocked(indexGroup, func) { - let lockDoc = await this._fetchAndLockIndexGroupDoc(indexGroup); - - try { - lockDoc = await func(lockDoc); - } finally { - await releaseLock(lockDoc); - } - }, }; }; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index bd31196dbb78b..b68faf7f75b99 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -14,7 +14,6 @@ import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/moc import { ScopedClusterClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { - IndexGroup, ReindexOperation, ReindexSavedObject, ReindexStatus, @@ -28,12 +27,7 @@ import { getMockVersionInfo } from '../__fixtures__/version'; import { esIndicesStateCheck } from '../es_indices_state_check'; import { versionService } from '../version'; -import { - isMlIndex, - isWatcherIndex, - ReindexService, - reindexServiceFactory, -} from './reindex_service'; +import { ReindexService, reindexServiceFactory } from './reindex_service'; const asApiResponse = (body: T): TransportResult => ({ @@ -69,9 +63,6 @@ describe('reindexService', () => { findAllByStatus: jest.fn(unimplemented('findAllInProgressOperations')), getFlatSettings: jest.fn(unimplemented('getFlatSettings')), cleanupChanges: jest.fn(), - incrementIndexGroupReindexes: jest.fn(unimplemented('incrementIndexGroupReindexes')), - decrementIndexGroupReindexes: jest.fn(unimplemented('decrementIndexGroupReindexes')), - runWhileIndexGroupLocked: jest.fn(async (group: string, f: any) => f({ attributes: {} })), }; clusterClient = elasticsearchServiceMock.createScopedClusterClient(); log = loggingSystemMock.create().get(); @@ -129,31 +120,6 @@ describe('reindexService', () => { }); }); - it('includes manage_ml for ML indices', async () => { - clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ has_all_requested: true }) - ); - - await service.hasRequiredPrivileges('.ml-anomalies'); - expect(clusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ - body: { - cluster: ['manage', 'manage_ml'], - index: [ - { - names: ['.ml-anomalies', `.reindexed-v${currentMajor}-ml-anomalies`], - allow_restricted_indices: true, - privileges: ['all'], - }, - { - names: ['.tasks'], - privileges: ['read', 'delete'], - }, - ], - }, - }); - }); - it('includes checking for permissions on the baseName which could be an alias', async () => { clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( // @ts-expect-error not full interface @@ -183,33 +149,6 @@ describe('reindexService', () => { }, }); }); - - it('includes manage_watcher for watcher indices', async () => { - clusterClient.asCurrentUser.security.hasPrivileges.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ - has_all_requested: true, - }) - ); - - await service.hasRequiredPrivileges('.watches'); - expect(clusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ - body: { - cluster: ['manage', 'manage_watcher'], - index: [ - { - names: ['.watches', `.reindexed-v${currentMajor}-watches`], - allow_restricted_indices: true, - privileges: ['all'], - }, - { - names: ['.tasks'], - privileges: ['read', 'delete'], - }, - ], - }, - }); - }); }); describe('detectReindexWarnings', () => { @@ -496,40 +435,6 @@ describe('reindexService', () => { }); }); - describe('isMlIndex', () => { - it('is false for non-ml indices', () => { - expect(isMlIndex('.literally-anything')).toBe(false); - }); - - it('is true for ML indices', () => { - expect(isMlIndex('.ml-state')).toBe(true); - expect(isMlIndex('.ml-anomalies')).toBe(true); - expect(isMlIndex('.ml-config')).toBe(true); - }); - - it('is true for ML re-indexed indices', () => { - expect(isMlIndex(`.reindexed-v${prevMajor}-ml-state`)).toBe(true); - expect(isMlIndex(`.reindexed-v${prevMajor}-ml-anomalies`)).toBe(true); - expect(isMlIndex(`.reindexed-v${prevMajor}-ml-config`)).toBe(true); - }); - }); - - describe('isWatcherIndex', () => { - it('is false for non-watcher indices', () => { - expect(isWatcherIndex('.literally-anything')).toBe(false); - }); - - it('is true for watcher indices', () => { - expect(isWatcherIndex('.watches')).toBe(true); - expect(isWatcherIndex('.triggered-watches')).toBe(true); - }); - - it('is true for watcher re-indexed indices', () => { - expect(isWatcherIndex(`.reindexed-v${prevMajor}-watches`)).toBe(true); - expect(isWatcherIndex(`.reindexed-v${prevMajor}-triggered-watches`)).toBe(true); - }); - }); - describe('state machine, lastCompletedStep ===', () => { const defaultAttributes = { indexName: 'myIndex', @@ -541,287 +446,6 @@ describe('reindexService', () => { mappings: { _doc: { properties: { timestampl: { type: 'date' } } } }, }; - describe('created', () => { - const reindexOp = { - id: '1', - attributes: { ...defaultAttributes, lastCompletedStep: ReindexStep.created }, - } as ReindexSavedObject; - - describe('ml behavior', () => { - const mlReindexOp = { - id: '2', - attributes: { ...reindexOp.attributes, indexName: '.ml-anomalies' }, - } as ReindexSavedObject; - - it('does nothing if index is not an ML index', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).not.toHaveBeenCalled(); - expect(actions.runWhileIndexGroupLocked).not.toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.nodes.info).not.toHaveBeenCalled(); - }); - - it('supports an already migrated ML index', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f() - ); - clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const mlReindexedOp = { - id: '2', - attributes: { - ...reindexOp.attributes, - indexName: `.reindexed-v${prevMajor}-ml-anomalies`, - }, - } as ReindexSavedObject; - const updatedOp = await service.processNextStep(mlReindexedOp); - - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).toHaveBeenCalled(); - expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('increments ML reindexes and calls ML stop endpoint', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f() - ); - - clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ nodes: { nodeX: { version: '6.7.0-alpha' } } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).toHaveBeenCalled(); - expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if ML reindexes cannot be incremented', async () => { - actions.incrementIndexGroupReindexes.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if ML doc cannot be locked', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if ML endpoint fails', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f() - ); - clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ nodes: { nodeX: { version: '6.7.0' } } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Could not stop ML jobs') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if not all nodes have been upgraded to 6.7.0', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f() - ); - clusterClient.asCurrentUser.nodes.info.mockResolvedValueOnce( - // @ts-expect-error not full interface - asApiResponse({ nodes: { nodeX: { version: '6.6.0' } } }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Some nodes are not on minimum version') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - // Should not have called ML endpoint at all - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: true, - }); - }); - }); - - describe('watcher behavior', () => { - const watcherReindexOp = { - id: '2', - attributes: { ...reindexOp.attributes, indexName: '.watches' }, - } as ReindexSavedObject; - - it('does nothing if index is not a watcher index', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).not.toHaveBeenCalled(); - expect(actions.runWhileIndexGroupLocked).not.toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalled(); - }); - - it('increments ML reindexes and calls watcher stop endpoint', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (type: string, f: any) => - f() - ); - clusterClient.asCurrentUser.watcher.stop.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(actions.incrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.watcher); - expect(actions.runWhileIndexGroupLocked).toHaveBeenCalled(); - expect(clusterClient.asCurrentUser.watcher.stop).toHaveBeenCalled(); - }); - - it('fails if watcher reindexes cannot be incremented', async () => { - actions.incrementIndexGroupReindexes.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalledWith({ - enabled: true, - }); - }); - - it('fails if watcher doc cannot be locked', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.stop).not.toHaveBeenCalled(); - }); - - it('fails if watcher endpoint fails', async () => { - actions.incrementIndexGroupReindexes.mockResolvedValueOnce(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (type: string, f: any) => - f() - ); - clusterClient.asCurrentUser.watcher.stop.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.created); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Could not stop Watcher') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.stop).toHaveBeenCalled(); - }); - }); - }); - - describe('indexConsumersStopped', () => { - const reindexOp = { - id: '1', - attributes: { - ...defaultAttributes, - lastCompletedStep: ReindexStep.indexGroupServicesStopped, - }, - } as ReindexSavedObject; - - it('blocks writes and updates lastCompletedStep', async () => { - clusterClient.asCurrentUser.indices.putSettings.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.readonly); - expect(clusterClient.asCurrentUser.indices.putSettings).toHaveBeenCalledWith({ - index: 'myIndex', - body: { blocks: { write: true } }, - }); - }); - - it('fails if setting updates are not acknowledged', async () => { - clusterClient.asCurrentUser.indices.putSettings.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - }); - - it('fails if setting updates fail', async () => { - clusterClient.asCurrentUser.indices.putSettings.mockRejectedValueOnce(new Error('blah!')); - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStopped - ); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage).not.toBeNull(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - }); - }); - describe('readonly', () => { const reindexOp = { id: '1', @@ -1129,216 +753,19 @@ describe('reindexService', () => { }); describe('aliasCreated', () => { - const reindexOp = { - id: '1', - attributes: { ...defaultAttributes, lastCompletedStep: ReindexStep.aliasCreated }, - } as ReindexSavedObject; - - describe('ml behavior', () => { - const mlReindexOp = { - id: '2', - attributes: { ...reindexOp.attributes, indexName: '.ml-anomalies' }, - } as ReindexSavedObject; - - it('does nothing if index is not an ML index', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalled(); - }); - - it('decrements ML reindexes and calls ML start endpoint if no remaining ML jobs', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 0 } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(actions.decrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.ml); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: false, - }); - }); - - it('does not call ML start endpoint if there are remaining ML jobs', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 2 } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: false, - }); - }); - - it('fails if ML reindexes cannot be decremented', async () => { - // Mock unable to lock ml doc - actions.decrementIndexGroupReindexes.mockRejectedValue(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: false, - }); - }); - - it('fails if ML doc cannot be locked', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - // Mock unable to lock ml doc - actions.runWhileIndexGroupLocked.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).not.toHaveBeenCalledWith({ - enabled: false, - }); - }); - - it('fails if ML endpoint fails', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 0 } }) - ); - clusterClient.asCurrentUser.ml.setUpgradeMode.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - const updatedOp = await service.processNextStep(mlReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Could not resume ML jobs') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.ml.setUpgradeMode).toHaveBeenCalledWith({ - enabled: false, - }); - }); - }); - - describe('watcher behavior', () => { - const watcherReindexOp = { - id: '2', - attributes: { ...reindexOp.attributes, indexName: '.watches' }, - } as ReindexSavedObject; - - it('does nothing if index is not a watcher index', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalled(); - }); - - it('decrements watcher reindexes and calls wathcer start endpoint if no remaining watcher reindexes', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 0 } }) - ); - clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(actions.decrementIndexGroupReindexes).toHaveBeenCalledWith(IndexGroup.watcher); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.watcher.start).toHaveBeenCalled(); - }); - - it('does not call watcher start endpoint if there are remaining watcher reindexes', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 2 } }) - ); - clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( - asApiResponse({ acknowledged: true }) - ); - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual( - ReindexStep.indexGroupServicesStarted - ); - expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); - }); - - it('fails if watcher reindexes cannot be decremented', async () => { - // Mock unable to lock watcher doc - actions.decrementIndexGroupReindexes.mockRejectedValue(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); - }); - - it('fails if watcher doc cannot be locked', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - // Mock unable to lock watcher doc - actions.runWhileIndexGroupLocked.mockRejectedValueOnce(new Error(`Can't lock!`)); - - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect(updatedOp.attributes.errorMessage!.includes(`Can't lock!`)).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.start).not.toHaveBeenCalledWith(); - }); - - it('fails if watcher endpoint fails', async () => { - actions.decrementIndexGroupReindexes.mockResolvedValue(); - actions.runWhileIndexGroupLocked.mockImplementationOnce(async (group: string, f: any) => - f({ attributes: { runningReindexCount: 0 } }) - ); - - clusterClient.asCurrentUser.watcher.start.mockResolvedValueOnce( - asApiResponse({ acknowledged: false }) - ); - const updatedOp = await service.processNextStep(watcherReindexOp); - expect(updatedOp.attributes.lastCompletedStep).toEqual(ReindexStep.aliasCreated); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.failed); - expect( - updatedOp.attributes.errorMessage!.includes('Could not start Watcher') - ).toBeTruthy(); - expect(log.error).toHaveBeenCalledWith(expect.any(String)); - expect(clusterClient.asCurrentUser.watcher.start).toHaveBeenCalled(); - }); - }); - }); - - describe('indexGroupServicesStarted', () => { const reindexOp = { id: '1', attributes: { ...defaultAttributes, - lastCompletedStep: ReindexStep.indexGroupServicesStarted, + lastCompletedStep: ReindexStep.aliasCreated, }, } as ReindexSavedObject; - it('sets to completed', async () => { - const updatedOp = await service.processNextStep(reindexOp); - expect(updatedOp.attributes.status).toEqual(ReindexStatus.completed); + it('sets reindex status as complete', async () => { + await service.processNextStep(reindexOp); + expect(actions.updateReindexOp).toHaveBeenCalledWith(reindexOp, { + status: ReindexStatus.completed, + }); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts index 77b5495bd4563..f9db1692ab1b7 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts @@ -11,7 +11,6 @@ import { first } from 'rxjs/operators'; import { LicensingPluginSetup } from '../../../../licensing/server'; import { - IndexGroup, ReindexSavedObject, ReindexStatus, ReindexStep, @@ -31,10 +30,6 @@ import { ReindexActions } from './reindex_actions'; import { error } from './error'; -const VERSION_REGEX = new RegExp(/^([1-9]+)\.([0-9]+)\.([0-9]+)/); -const ML_INDICES = ['.ml-state', '.ml-anomalies', '.ml-config']; -const WATCHER_INDICES = ['.watches', '.triggered-watches']; - export interface ReindexService { /** * Checks whether or not the user has proper privileges required to reindex this index. @@ -49,12 +44,6 @@ export interface ReindexService { */ detectReindexWarnings(indexName: string): Promise; - /** - * Returns an IndexGroup if the index belongs to one, otherwise undefined. - * @param indexName - */ - getIndexGroup(indexName: string): IndexGroup | undefined; - /** * Creates a new reindex operation for a given index. * @param indexName @@ -135,83 +124,6 @@ export const reindexServiceFactory = ( licensing: LicensingPluginSetup ): ReindexService => { // ------ Utility functions - - /** - * If the index is a ML index that will cause jobs to fail when set to readonly, - * turn on 'upgrade mode' to pause all ML jobs. - * @param reindexOp - */ - const stopMlJobs = async () => { - await actions.incrementIndexGroupReindexes(IndexGroup.ml); - await actions.runWhileIndexGroupLocked(IndexGroup.ml, async (mlDoc) => { - await validateNodesMinimumVersion(6, 7); - - const { body } = await esClient.ml.setUpgradeMode({ - enabled: true, - }); - - if (!body.acknowledged) { - throw new Error(`Could not stop ML jobs`); - } - - return mlDoc; - }); - }; - - /** - * Resumes ML jobs if there are no more remaining reindex operations. - */ - const resumeMlJobs = async () => { - await actions.decrementIndexGroupReindexes(IndexGroup.ml); - await actions.runWhileIndexGroupLocked(IndexGroup.ml, async (mlDoc) => { - if (mlDoc.attributes.runningReindexCount === 0) { - const { body } = await esClient.ml.setUpgradeMode({ - enabled: false, - }); - - if (!body.acknowledged) { - throw new Error(`Could not resume ML jobs`); - } - } - - return mlDoc; - }); - }; - - /** - * Stops Watcher in Elasticsearch. - */ - const stopWatcher = async () => { - await actions.incrementIndexGroupReindexes(IndexGroup.watcher); - await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async (watcherDoc) => { - const { body } = await esClient.watcher.stop(); - - if (!body.acknowledged) { - throw new Error('Could not stop Watcher'); - } - - return watcherDoc; - }); - }; - - /** - * Starts Watcher in Elasticsearch. - */ - const startWatcher = async () => { - await actions.decrementIndexGroupReindexes(IndexGroup.watcher); - await actions.runWhileIndexGroupLocked(IndexGroup.watcher, async (watcherDoc) => { - if (watcherDoc.attributes.runningReindexCount === 0) { - const { body } = await esClient.watcher.start(); - - if (!body.acknowledged) { - throw new Error('Could not start Watcher'); - } - } - - return watcherDoc; - }); - }; - const cleanupChanges = async (reindexOp: ReindexSavedObject) => { // Cancel reindex task if it was started but not completed if (reindexOp.attributes.lastCompletedStep === ReindexStep.reindexStarted) { @@ -239,48 +151,11 @@ export const reindexServiceFactory = ( }); } - // Resume consumers if we ever got past this point. - if (reindexOp.attributes.lastCompletedStep >= ReindexStep.indexGroupServicesStopped) { - await resumeIndexGroupServices(reindexOp); - } - return reindexOp; }; // ------ Functions used to process the state machine - const validateNodesMinimumVersion = async (minMajor: number, minMinor: number) => { - const { body: nodesResponse } = await esClient.nodes.info(); - - const outDatedNodes = Object.values(nodesResponse.nodes).filter((node: any) => { - const matches = node.version.match(VERSION_REGEX); - const major = parseInt(matches[1], 10); - const minor = parseInt(matches[2], 10); - - // All ES nodes must be >= 6.7.0 to pause ML jobs - return !(major > minMajor || (major === minMajor && minor >= minMinor)); - }); - - if (outDatedNodes.length > 0) { - const nodeList = JSON.stringify(outDatedNodes.map((n: any) => n.name)); - throw new Error( - `Some nodes are not on minimum version (${minMajor}.${minMinor}.0) required: ${nodeList}` - ); - } - }; - - const stopIndexGroupServices = async (reindexOp: ReindexSavedObject) => { - if (isMlIndex(reindexOp.attributes.indexName)) { - await stopMlJobs(); - } else if (isWatcherIndex(reindexOp.attributes.indexName)) { - await stopWatcher(); - } - - return actions.updateReindexOp(reindexOp, { - lastCompletedStep: ReindexStep.indexGroupServicesStopped, - }); - }; - /** * Sets the original index as readonly so new data can be indexed until the reindex * is completed. @@ -476,23 +351,6 @@ export const reindexServiceFactory = ( }); }; - const resumeIndexGroupServices = async (reindexOp: ReindexSavedObject) => { - if (isMlIndex(reindexOp.attributes.indexName)) { - await resumeMlJobs(); - } else if (isWatcherIndex(reindexOp.attributes.indexName)) { - await startWatcher(); - } - - // Only change the status if we're still in-progress (this function is also called when the reindex fails or is cancelled) - if (reindexOp.attributes.status === ReindexStatus.inProgress) { - return actions.updateReindexOp(reindexOp, { - lastCompletedStep: ReindexStep.indexGroupServicesStarted, - }); - } else { - return reindexOp; - } - }; - // ------ The service itself return { @@ -537,14 +395,6 @@ export const reindexServiceFactory = ( ], } as any; - if (isMlIndex(indexName)) { - body.cluster = [...body.cluster, 'manage_ml']; - } - - if (isWatcherIndex(indexName)) { - body.cluster = [...body.cluster, 'manage_watcher']; - } - const { body: resp } = await esClient.security.hasPrivileges({ body, }); @@ -561,14 +411,6 @@ export const reindexServiceFactory = ( } }, - getIndexGroup(indexName: string) { - if (isMlIndex(indexName)) { - return IndexGroup.ml; - } else if (isWatcherIndex(indexName)) { - return IndexGroup.watcher; - } - }, - async createReindexOperation(indexName: string, opts?: { enqueue: boolean }) { const { body: indexExists } = await esClient.indices.exists({ index: indexName }); if (!indexExists) { @@ -636,9 +478,6 @@ export const reindexServiceFactory = ( try { switch (lockedReindexOp.attributes.lastCompletedStep) { case ReindexStep.created: - lockedReindexOp = await stopIndexGroupServices(lockedReindexOp); - break; - case ReindexStep.indexGroupServicesStopped: lockedReindexOp = await setReadonly(lockedReindexOp); break; case ReindexStep.readonly: @@ -654,12 +493,10 @@ export const reindexServiceFactory = ( lockedReindexOp = await switchAlias(lockedReindexOp); break; case ReindexStep.aliasCreated: - lockedReindexOp = await resumeIndexGroupServices(lockedReindexOp); - break; - case ReindexStep.indexGroupServicesStarted: lockedReindexOp = await actions.updateReindexOp(lockedReindexOp, { status: ReindexStatus.completed, }); + break; default: break; } @@ -767,13 +604,3 @@ export const reindexServiceFactory = ( }, }; }; - -export const isMlIndex = (indexName: string) => { - const sourceName = sourceNameForIndex(indexName); - return ML_INDICES.indexOf(sourceName) >= 0; -}; - -export const isWatcherIndex = (indexName: string) => { - const sourceName = sourceNameForIndex(indexName); - return WATCHER_INDICES.indexOf(sourceName) >= 0; -}; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts index c598da93388c3..3491c92ef5953 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts @@ -7,6 +7,7 @@ import { IClusterClient, Logger, SavedObjectsClientContract, FakeRequest } from 'src/core/server'; import moment from 'moment'; +import { SecurityPluginStart } from '../../../../security/server'; import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; import { Credential, CredentialStore } from './credential_store'; import { reindexActionsFactory } from './reindex_actions'; @@ -46,15 +47,19 @@ export class ReindexWorker { private inProgressOps: ReindexSavedObject[] = []; private readonly reindexService: ReindexService; private readonly log: Logger; + private readonly security: SecurityPluginStart; constructor( private client: SavedObjectsClientContract, private credentialStore: CredentialStore, private clusterClient: IClusterClient, log: Logger, - private licensing: LicensingPluginSetup + private licensing: LicensingPluginSetup, + security: SecurityPluginStart ) { this.log = log.get('reindex_worker'); + this.security = security; + if (ReindexWorker.workerSingleton) { throw new Error(`More than one ReindexWorker cannot be created.`); } @@ -171,7 +176,11 @@ export class ReindexWorker { firstOpInQueue.attributes.indexName ); // Re-associate the credentials - this.credentialStore.set(firstOpInQueue, credential); + this.credentialStore.update({ + reindexOp: firstOpInQueue, + security: this.security, + credential, + }); } } @@ -223,7 +232,7 @@ export class ReindexWorker { reindexOp = await swallowExceptions(service.processNextStep, this.log)(reindexOp); // Update credential store with most recent state. - this.credentialStore.set(reindexOp, credential); + this.credentialStore.update({ reindexOp, security: this.security, credential }); }; } diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts deleted file mode 100644 index caff78390b9d1..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; - -import { upsertUIOpenOption } from './es_ui_open_apis'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { - describe('Upsert UIOpen Option', () => { - it('call saved objects internal repository with the correct info', async () => { - const internalRepo = savedObjectsRepositoryMock.create(); - - await upsertUIOpenOption({ - overview: true, - elasticsearch: true, - kibana: true, - savedObjects: { createInternalRepository: () => internalRepo } as any, - }); - - expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(3); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.overview'] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.elasticsearch'] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - ['ui_open.kibana'] - ); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts deleted file mode 100644 index 3d463fe4b03ed..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_open_apis.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsServiceStart } from 'src/core/server'; -import { - UIOpen, - UIOpenOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../common/types'; - -interface IncrementUIOpenDependencies { - uiOpenOptionCounter: UIOpenOption; - savedObjects: SavedObjectsServiceStart; -} - -async function incrementUIOpenOptionCounter({ - savedObjects, - uiOpenOptionCounter, -}: IncrementUIOpenDependencies) { - const internalRepository = savedObjects.createInternalRepository(); - - await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ - `ui_open.${uiOpenOptionCounter}`, - ]); -} - -type UpsertUIOpenOptionDependencies = UIOpen & { savedObjects: SavedObjectsServiceStart }; - -export async function upsertUIOpenOption({ - overview, - elasticsearch, - savedObjects, - kibana, -}: UpsertUIOpenOptionDependencies): Promise { - if (overview) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'overview' }); - } - - if (elasticsearch) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'elasticsearch' }); - } - - if (kibana) { - await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'kibana' }); - } - - return { - overview, - elasticsearch, - kibana, - }; -} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts deleted file mode 100644 index 6a05e8a697bb8..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.test.ts +++ /dev/null @@ -1,52 +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 { savedObjectsRepositoryMock } from 'src/core/server/mocks'; -import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE } from '../../../common/types'; -import { upsertUIReindexOption } from './es_ui_reindex_apis'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { - describe('Upsert UIReindex Option', () => { - it('call saved objects internal repository with the correct info', async () => { - const internalRepo = savedObjectsRepositoryMock.create(); - await upsertUIReindexOption({ - close: true, - open: true, - start: true, - stop: true, - savedObjects: { createInternalRepository: () => internalRepo } as any, - }); - - expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(4); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.close`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.open`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.start`] - ); - expect(internalRepo.incrementCounter).toHaveBeenCalledWith( - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID, - [`ui_reindex.stop`] - ); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts deleted file mode 100644 index caee1a58a4006..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/es_ui_reindex_apis.ts +++ /dev/null @@ -1,63 +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 { SavedObjectsServiceStart } from 'src/core/server'; -import { - UIReindex, - UIReindexOption, - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, -} from '../../../common/types'; - -interface IncrementUIReindexOptionDependencies { - uiReindexOptionCounter: UIReindexOption; - savedObjects: SavedObjectsServiceStart; -} - -async function incrementUIReindexOptionCounter({ - savedObjects, - uiReindexOptionCounter, -}: IncrementUIReindexOptionDependencies) { - const internalRepository = savedObjects.createInternalRepository(); - - await internalRepository.incrementCounter(UPGRADE_ASSISTANT_TYPE, UPGRADE_ASSISTANT_DOC_ID, [ - `ui_reindex.${uiReindexOptionCounter}`, - ]); -} - -type UpsertUIReindexOptionDepencies = UIReindex & { savedObjects: SavedObjectsServiceStart }; - -export async function upsertUIReindexOption({ - start, - close, - open, - stop, - savedObjects, -}: UpsertUIReindexOptionDepencies): Promise { - if (close) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'close' }); - } - - if (open) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'open' }); - } - - if (start) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'start' }); - } - - if (stop) { - await incrementUIReindexOptionCounter({ savedObjects, uiReindexOptionCounter: 'stop' }); - } - - return { - close, - open, - start, - stop, - }; -} diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 50c5b358aa5cb..34d329557f11e 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -47,26 +47,6 @@ describe('Upgrade Assistant Usage Collector', () => { }; dependencies = { usageCollection, - savedObjects: { - createInternalRepository: jest.fn().mockImplementation(() => { - return { - get: () => { - return { - attributes: { - 'ui_open.overview': 10, - 'ui_open.elasticsearch': 20, - 'ui_open.kibana': 15, - 'ui_reindex.close': 1, - 'ui_reindex.open': 4, - 'ui_reindex.start': 2, - 'ui_reindex.stop': 1, - 'ui_reindex.not_defined': 1, - }, - }; - }, - }; - }), - }, elasticsearch: { client: clusterClient, }, @@ -91,17 +71,6 @@ describe('Upgrade Assistant Usage Collector', () => { callClusterStub ); expect(upgradeAssistantStats).toEqual({ - ui_open: { - overview: 10, - elasticsearch: 20, - kibana: 15, - }, - ui_reindex: { - close: 1, - open: 4, - start: 2, - stop: 1, - }, features: { deprecation_logging: { enabled: true, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 56932f5e54b06..c535cd14f104d 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -5,43 +5,14 @@ * 2.0. */ -import { get } from 'lodash'; -import { - ElasticsearchClient, - ElasticsearchServiceStart, - ISavedObjectsRepository, - SavedObjectsServiceStart, -} from 'src/core/server'; +import { ElasticsearchClient, ElasticsearchServiceStart } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { - UPGRADE_ASSISTANT_DOC_ID, - UPGRADE_ASSISTANT_TYPE, - UpgradeAssistantTelemetry, - UpgradeAssistantTelemetrySavedObject, - UpgradeAssistantTelemetrySavedObjectAttributes, -} from '../../../common/types'; +import { UpgradeAssistantTelemetry } from '../../../common/types'; import { isDeprecationLogIndexingEnabled, isDeprecationLoggingEnabled, } from '../es_deprecation_logging_apis'; -async function getSavedObjectAttributesFromRepo( - savedObjectsRepository: ISavedObjectsRepository, - docType: string, - docID: string -) { - try { - return ( - await savedObjectsRepository.get( - docType, - docID - ) - ).attributes; - } catch (e) { - return null; - } -} - async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): Promise { try { const { body: loggerDeprecationCallResult } = await esClient.cluster.getSettings({ @@ -57,58 +28,14 @@ async function getDeprecationLoggingStatusValue(esClient: ElasticsearchClient): } } -export async function fetchUpgradeAssistantMetrics( - { client: esClient }: ElasticsearchServiceStart, - savedObjects: SavedObjectsServiceStart -): Promise { - const savedObjectsRepository = savedObjects.createInternalRepository(); - const upgradeAssistantSOAttributes = await getSavedObjectAttributesFromRepo( - savedObjectsRepository, - UPGRADE_ASSISTANT_TYPE, - UPGRADE_ASSISTANT_DOC_ID - ); +export async function fetchUpgradeAssistantMetrics({ + client: esClient, +}: ElasticsearchServiceStart): Promise { const deprecationLoggingStatusValue = await getDeprecationLoggingStatusValue( esClient.asInternalUser ); - const getTelemetrySavedObject = ( - upgradeAssistantTelemetrySavedObjectAttrs: UpgradeAssistantTelemetrySavedObjectAttributes | null - ): UpgradeAssistantTelemetrySavedObject => { - const defaultTelemetrySavedObject = { - ui_open: { - overview: 0, - elasticsearch: 0, - kibana: 0, - }, - ui_reindex: { - close: 0, - open: 0, - start: 0, - stop: 0, - }, - }; - - if (!upgradeAssistantTelemetrySavedObjectAttrs) { - return defaultTelemetrySavedObject; - } - - return { - ui_open: { - overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0), - elasticsearch: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.elasticsearch', 0), - kibana: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.kibana', 0), - }, - ui_reindex: { - close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0), - open: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.open', 0), - start: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.start', 0), - stop: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.stop', 0), - }, - } as UpgradeAssistantTelemetrySavedObject; - }; - return { - ...getTelemetrySavedObject(upgradeAssistantSOAttributes), features: { deprecation_logging: { enabled: deprecationLoggingStatusValue, @@ -119,14 +46,12 @@ export async function fetchUpgradeAssistantMetrics( interface Dependencies { elasticsearch: ElasticsearchServiceStart; - savedObjects: SavedObjectsServiceStart; usageCollection: UsageCollectionSetup; } export function registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, - savedObjects, }: Dependencies) { const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ @@ -143,34 +68,8 @@ export function registerUpgradeAssistantUsageCollector({ }, }, }, - ui_open: { - elasticsearch: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the list of Elasticsearch deprecations.', - }, - }, - overview: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the Overview page.', - }, - }, - kibana: { - type: 'long', - _meta: { - description: 'Number of times a user viewed the list of Kibana deprecations', - }, - }, - }, - ui_reindex: { - close: { type: 'long' }, - open: { type: 'long' }, - start: { type: 'long' }, - stop: { type: 'long' }, - }, }, - fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects), + fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch), }); usageCollection.registerCollector(upgradeAssistantUsageCollector); diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 870bd6b985661..717f03758f825 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -6,7 +6,6 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; - import { Plugin, CoreSetup, @@ -16,10 +15,13 @@ import { SavedObjectsClient, SavedObjectsServiceStart, } from '../../../../src/core/server'; +import { SecurityPluginStart } from '../../security/server'; import { InfraPluginSetup } from '../../infra/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; +import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX } from '../common/constants'; import { CredentialStore, credentialStoreFactory } from './lib/reindexing/credential_store'; import { ReindexWorker } from './lib/reindexing'; @@ -32,7 +34,7 @@ import { reindexOperationSavedObjectType, mlSavedObjectType, } from './saved_object_types'; -import { DEPRECATION_LOGS_SOURCE_ID, DEPRECATION_LOGS_INDEX_PATTERN } from '../common/constants'; +import { handleEsError } from './shared_imports'; import { RouteDependencies } from './types'; @@ -41,6 +43,11 @@ interface PluginsSetup { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; infra: InfraPluginSetup; + security?: SecurityPluginSetup; +} + +interface PluginsStart { + security: SecurityPluginStart; } export class UpgradeAssistantServerPlugin implements Plugin { @@ -53,11 +60,12 @@ export class UpgradeAssistantServerPlugin implements Plugin { // Properties set at start private savedObjectsServiceStart?: SavedObjectsServiceStart; + private securityPluginStart?: SecurityPluginStart; private worker?: ReindexWorker; constructor({ logger, env }: PluginInitializerContext) { this.logger = logger.get(); - this.credentialStore = credentialStoreFactory(); + this.credentialStore = credentialStoreFactory(this.logger); this.kibanaVersion = env.packageInfo.version; } @@ -70,7 +78,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { setup( { http, getStartServices, savedObjects }: CoreSetup, - { usageCollection, features, licensing, infra }: PluginsSetup + { usageCollection, features, licensing, infra, security }: PluginsSetup ) { this.licensing = licensing; @@ -93,12 +101,12 @@ export class UpgradeAssistantServerPlugin implements Plugin { // We need to initialize the deprecation logs plugin so that we can // navigate from this app to the observability app using a source_id. - infra.defineInternalSourceConfiguration(DEPRECATION_LOGS_SOURCE_ID, { + infra?.defineInternalSourceConfiguration(DEPRECATION_LOGS_SOURCE_ID, { name: 'deprecationLogs', description: 'deprecation logs', logIndices: { type: 'index_name', - indexName: DEPRECATION_LOGS_INDEX_PATTERN, + indexName: DEPRECATION_LOGS_INDEX, }, logColumns: [ { timestampColumn: { id: 'timestampField' } }, @@ -119,6 +127,13 @@ export class UpgradeAssistantServerPlugin implements Plugin { } return this.savedObjectsServiceStart; }, + getSecurityPlugin: () => this.securityPluginStart, + lib: { + handleEsError, + }, + config: { + isSecurityEnabled: () => security !== undefined && security.license.isEnabled(), + }, }; // Initialize version service with current kibana version @@ -127,18 +142,18 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerRoutes(dependencies, this.getWorker.bind(this)); if (usageCollection) { - getStartServices().then(([{ savedObjects: savedObjectsService, elasticsearch }]) => { + getStartServices().then(([{ elasticsearch }]) => { registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, - savedObjects: savedObjectsService, }); }); } } - start({ savedObjects, elasticsearch }: CoreStart) { + start({ savedObjects, elasticsearch }: CoreStart, { security }: PluginsStart) { this.savedObjectsServiceStart = savedObjects; + this.securityPluginStart = security; // The ReindexWorker uses a map of request headers that contain the authentication credentials // for a given reindex. We cannot currently store these in an the .kibana index b/c we do not @@ -155,6 +170,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { savedObjects: new SavedObjectsClient( this.savedObjectsServiceStart.createInternalRepository() ), + security: this.securityPluginStart, }); this.worker.start(); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts index d3a36835d12be..c77f3a6661ebe 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/request.mock.ts @@ -8,6 +8,7 @@ export const createRequestMock = (opts?: { headers?: any; params?: Record; + query?: Record; body?: Record; }) => { return Object.assign({ headers: {} }, opts || {}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/app.ts b/x-pack/plugins/upgrade_assistant/server/routes/app.ts new file mode 100644 index 0000000000000..682dc83410f81 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/app.ts @@ -0,0 +1,80 @@ +/* + * 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 { API_BASE_PATH, DEPRECATION_LOGS_INDEX } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { Privileges } from '../shared_imports'; +import { RouteDependencies } from '../types'; + +const extractMissingPrivileges = ( + privilegesObject: { [key: string]: Record } = {} +): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (Object.values(privilegesObject[privilegeName]).some((e) => !e)) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export function registerAppRoutes({ + router, + lib: { handleEsError }, + config: { isSecurityEnabled }, +}: RouteDependencies) { + router.get( + { + path: `${API_BASE_PATH}/privileges`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + index: [], + }, + }; + + if (!isSecurityEnabled()) { + return response.ok({ body: privilegesResult }); + } + + try { + const { + body: { has_all_requested: hasAllPrivileges, index }, + } = await client.asCurrentUser.security.hasPrivileges({ + body: { + index: [ + { + names: [DEPRECATION_LOGS_INDEX], + privileges: ['read'], + }, + ], + }, + }); + + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.index = extractMissingPrivileges(index); + } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + return response.ok({ body: privilegesResult }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts b/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts new file mode 100644 index 0000000000000..5d3ab7c854e7b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/cloud_backup_status.ts @@ -0,0 +1,54 @@ +/* + * 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 { API_BASE_PATH, CLOUD_SNAPSHOT_REPOSITORY } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerCloudBackupStatusRoutes({ + router, + lib: { handleEsError }, +}: RouteDependencies) { + // GET most recent Cloud snapshot + router.get( + { path: `${API_BASE_PATH}/cloud_backup_status`, validate: false }, + versionCheckHandlerWrapper(async (context, request, response) => { + const { client: clusterClient } = context.core.elasticsearch; + + try { + const { + body: { snapshots }, + } = await clusterClient.asCurrentUser.snapshot.get({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: '_all', + ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. + // @ts-expect-error @elastic/elasticsearch "desc" is a new param + order: 'desc', + sort: 'start_time', + size: 1, + }); + + let isBackedUp = false; + let lastBackupTime; + + if (snapshots && snapshots[0]) { + isBackedUp = true; + lastBackupTime = snapshots![0].start_time; + } + + return response.ok({ + body: { + isBackedUp, + lastBackupTime, + }, + }); + } catch (error) { + return handleEsError({ error, response }); + } + }) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/cluster_upgrade_status.ts b/x-pack/plugins/upgrade_assistant/server/routes/cluster_upgrade_status.ts new file mode 100644 index 0000000000000..4ae1205d2daef --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/cluster_upgrade_status.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 { API_BASE_PATH } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; + +export function registerClusterUpgradeStatusRoutes({ router }: RouteDependencies) { + router.get( + { path: `${API_BASE_PATH}/cluster_upgrade_status`, validate: false }, + // We're just depending on the version check to return a 426. + // Otherwise we just return a 200. + versionCheckHandlerWrapper(async (context, request, response) => { + return response.ok(); + }) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts index 1d51666dec3e5..89d4e4cb398c6 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.test.ts @@ -8,6 +8,7 @@ import { kibanaResponseFactory } from 'src/core/server'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; +import { handleEsError } from '../shared_imports'; jest.mock('../lib/es_version_precheck', () => ({ versionCheckHandlerWrapper: (a: any) => a, @@ -28,6 +29,7 @@ describe('deprecation logging API', () => { mockRouter = createMockRouter(); routeDependencies = { router: mockRouter, + lib: { handleEsError }, }; registerDeprecationLoggingRoutes(routeDependencies); }); @@ -43,7 +45,7 @@ describe('deprecation logging API', () => { .getSettings as jest.Mock ).mockResolvedValue({ body: { - default: { + defaults: { cluster: { deprecation_indexing: { enabled: 'true' } }, }, }, @@ -65,7 +67,7 @@ describe('deprecation logging API', () => { ( routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster .getSettings as jest.Mock - ).mockRejectedValue(new Error(`scary error!`)); + ).mockRejectedValue(new Error('scary error!')); await expect( routeDependencies.router.getHandler({ method: 'get', @@ -82,7 +84,7 @@ describe('deprecation logging API', () => { .putSettings as jest.Mock ).mockResolvedValue({ body: { - default: { + defaults: { logger: { deprecation: 'WARN' }, cluster: { deprecation_indexing: { enabled: 'true' } }, }, @@ -104,7 +106,7 @@ describe('deprecation logging API', () => { ( routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.cluster .putSettings as jest.Mock - ).mockRejectedValue(new Error(`scary error!`)); + ).mockRejectedValue(new Error('scary error!')); await expect( routeDependencies.router.getHandler({ method: 'put', @@ -113,4 +115,103 @@ describe('deprecation logging API', () => { ).rejects.toThrow('scary error!'); }); }); + + describe('GET /api/upgrade_assistant/deprecation_logging/count', () => { + const MOCK_FROM_DATE = '2021-08-23T07:32:34.782Z'; + + it('returns count of deprecations', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.indices.exists as jest.Mock + ).mockResolvedValue({ + body: true, + }); + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.count as jest.Mock + ).mockResolvedValue({ + body: { count: 10 }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging/count', + })( + routeHandlerContextMock, + createRequestMock({ query: { from: MOCK_FROM_DATE } }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ count: 10 }); + }); + + it('returns zero matches when deprecation logs index is not created', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.indices.exists as jest.Mock + ).mockResolvedValue({ + body: false, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging/count', + })( + routeHandlerContextMock, + createRequestMock({ query: { from: MOCK_FROM_DATE } }), + kibanaResponseFactory + ); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ count: 0 }); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.indices.exists as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/deprecation_logging/count', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); + + describe('DELETE /api/upgrade_assistant/deprecation_logging/cache', () => { + it('returns ok if if the cache was deleted', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockResolvedValue({ + body: 'ok', + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'delete', + pathPattern: '/api/upgrade_assistant/deprecation_logging/cache', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'DELETE', + path: '/_logging/deprecation_cache', + }); + expect(resp.payload).toEqual('ok'); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'delete', + pathPattern: '/api/upgrade_assistant/deprecation_logging/cache', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts index fb2a5b559e5a9..5d7f0f67b0ca9 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/deprecation_logging.ts @@ -14,8 +14,12 @@ import { } from '../lib/es_deprecation_logging_apis'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; import { RouteDependencies } from '../types'; +import { DEPRECATION_LOGS_INDEX } from '../../common/constants'; -export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) { +export function registerDeprecationLoggingRoutes({ + router, + lib: { handleEsError }, +}: RouteDependencies) { router.get( { path: `${API_BASE_PATH}/deprecation_logging`, @@ -31,8 +35,12 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) request, response ) => { - const result = await getDeprecationLoggingStatus(client); - return response.ok({ body: result }); + try { + const result = await getDeprecationLoggingStatus(client); + return response.ok({ body: result }); + } catch (error) { + return handleEsError({ error, response }); + } } ) ); @@ -56,10 +64,92 @@ export function registerDeprecationLoggingRoutes({ router }: RouteDependencies) request, response ) => { - const { isEnabled } = request.body as { isEnabled: boolean }; - return response.ok({ - body: await setDeprecationLogging(client, isEnabled), - }); + try { + const { isEnabled } = request.body as { isEnabled: boolean }; + return response.ok({ + body: await setDeprecationLogging(client, isEnabled), + }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); + + router.get( + { + path: `${API_BASE_PATH}/deprecation_logging/count`, + validate: { + query: schema.object({ + from: schema.string(), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const { body: indexExists } = await client.asCurrentUser.indices.exists({ + index: DEPRECATION_LOGS_INDEX, + }); + + if (!indexExists) { + return response.ok({ body: { count: 0 } }); + } + + const { body } = await client.asCurrentUser.count({ + index: DEPRECATION_LOGS_INDEX, + body: { + query: { + range: { + '@timestamp': { + gte: request.query.from, + }, + }, + }, + }, + }); + + return response.ok({ body: { count: body.count } }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); + + router.delete( + { + path: `${API_BASE_PATH}/deprecation_logging/cache`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + await client.asCurrentUser.transport.request({ + method: 'DELETE', + path: '/_logging/deprecation_cache', + }); + + return response.ok({ body: 'ok' }); + } catch (error) { + return handleEsError({ error, response }); + } } ) ); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.test.ts index bea74f116e0e2..4047ce827acbc 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.test.ts @@ -6,6 +6,8 @@ */ import { kibanaResponseFactory } from 'src/core/server'; + +import { handleEsError } from '../shared_imports'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; @@ -33,6 +35,7 @@ describe('ES deprecations API', () => { mockRouter = createMockRouter(); routeDependencies = { router: mockRouter, + lib: { handleEsError }, }; registerESDeprecationRoutes(routeDependencies); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.ts b/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.ts index eb0ade26de766..98089e34bdca1 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/es_deprecations.ts @@ -11,9 +11,13 @@ import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; import { RouteDependencies } from '../types'; import { reindexActionsFactory } from '../lib/reindexing/reindex_actions'; import { reindexServiceFactory } from '../lib/reindexing'; -import { handleEsError } from '../shared_imports'; -export function registerESDeprecationRoutes({ router, licensing, log }: RouteDependencies) { +export function registerESDeprecationRoutes({ + router, + lib: { handleEsError }, + licensing, + log, +}: RouteDependencies) { router.get( { path: `${API_BASE_PATH}/es_deprecations`, @@ -50,8 +54,8 @@ export function registerESDeprecationRoutes({ router, licensing, log }: RouteDep return response.ok({ body: status, }); - } catch (e) { - return handleEsError({ error: e, response }); + } catch (error) { + return handleEsError({ error, response }); } } ) diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts index 2f8cdd2aba808..995e3a46cef0e 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.test.ts @@ -6,6 +6,8 @@ */ import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { handleEsError } from '../shared_imports'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; import { registerMlSnapshotRoutes } from './ml_snapshots'; @@ -26,6 +28,7 @@ describe('ML snapshots APIs', () => { mockRouter = createMockRouter(); routeDependencies = { router: mockRouter, + lib: { handleEsError }, }; registerMlSnapshotRoutes(routeDependencies); }); @@ -172,6 +175,28 @@ describe('ML snapshots APIs', () => { }); }); + describe('GET /api/upgrade_assistant/ml_upgrade_mode', () => { + it('Retrieves ml upgrade mode', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.ml.info as jest.Mock + ).mockResolvedValue({ + body: { + upgrade_mode: true, + }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/ml_upgrade_mode', + })(routeHandlerContextMock, createRequestMock({}), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect(resp.payload).toEqual({ + mlUpgradeModeEnabled: true, + }); + }); + }); + describe('GET /api/upgrade_assistant/ml_snapshots/:jobId/:snapshotId', () => { it('returns "idle" status if saved object does not exist', async () => { ( diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts index 65e707339d67c..fa6af0f5e4228 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts @@ -11,7 +11,6 @@ import { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server' import { API_BASE_PATH } from '../../common/constants'; import { MlOperation, ML_UPGRADE_OP_TYPE } from '../../common/types'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; -import { handleEsError } from '../shared_imports'; import { RouteDependencies } from '../types'; const findMlOperation = async ( @@ -99,7 +98,7 @@ const verifySnapshotUpgrade = async ( } }; -export function registerMlSnapshotRoutes({ router }: RouteDependencies) { +export function registerMlSnapshotRoutes({ router, lib: { handleEsError } }: RouteDependencies) { // Upgrade ML model snapshot router.post( { @@ -147,8 +146,8 @@ export function registerMlSnapshotRoutes({ router }: RouteDependencies) { status: body.completed === true ? 'complete' : 'in_progress', }, }); - } catch (e) { - return handleEsError({ error: e, response }); + } catch (error) { + return handleEsError({ error, response }); } } ) @@ -301,6 +300,37 @@ export function registerMlSnapshotRoutes({ router }: RouteDependencies) { ) ); + // Get the ml upgrade mode + router.get( + { + path: `${API_BASE_PATH}/ml_upgrade_mode`, + validate: false, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client: esClient }, + }, + }, + request, + response + ) => { + try { + const { body: mlInfo } = await esClient.asCurrentUser.ml.info(); + + return response.ok({ + body: { + mlUpgradeModeEnabled: mlInfo.upgrade_mode, + }, + }); + } catch (e) { + return handleEsError({ error: e, response }); + } + } + ) + ); + // Delete ML model snapshot router.delete( { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts index 332db10805692..b6c8850376684 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/register_routes.ts @@ -7,20 +7,27 @@ import { RouteDependencies } from '../types'; +import { registerAppRoutes } from './app'; +import { registerCloudBackupStatusRoutes } from './cloud_backup_status'; +import { registerClusterUpgradeStatusRoutes } from './cluster_upgrade_status'; +import { registerSystemIndicesMigrationRoutes } from './system_indices_migration'; import { registerESDeprecationRoutes } from './es_deprecations'; import { registerDeprecationLoggingRoutes } from './deprecation_logging'; -import { registerReindexIndicesRoutes } from './reindex_indices'; -import { registerTelemetryRoutes } from './telemetry'; +import { registerReindexIndicesRoutes, registerBatchReindexIndicesRoutes } from './reindex_indices'; import { registerUpdateSettingsRoute } from './update_index_settings'; import { registerMlSnapshotRoutes } from './ml_snapshots'; import { ReindexWorker } from '../lib/reindexing'; import { registerUpgradeStatusRoute } from './status'; export function registerRoutes(dependencies: RouteDependencies, getWorker: () => ReindexWorker) { + registerAppRoutes(dependencies); + registerCloudBackupStatusRoutes(dependencies); + registerClusterUpgradeStatusRoutes(dependencies); + registerSystemIndicesMigrationRoutes(dependencies); registerESDeprecationRoutes(dependencies); registerDeprecationLoggingRoutes(dependencies); registerReindexIndicesRoutes(dependencies, getWorker); - registerTelemetryRoutes(dependencies); + registerBatchReindexIndicesRoutes(dependencies, getWorker); registerUpdateSettingsRoute(dependencies); registerMlSnapshotRoutes(dependencies); // Route for cloud to retrieve the upgrade status for ES and Kibana diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.test.ts new file mode 100644 index 0000000000000..961b63b30f4ea --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.test.ts @@ -0,0 +1,192 @@ +/* + * 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 { kibanaResponseFactory } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { licensingMock } from '../../../../licensing/server/mocks'; +import { securityMock } from '../../../../security/server/mocks'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from '../__mocks__/routes.mock'; +import { createRequestMock } from '../__mocks__/request.mock'; +import { handleEsError } from '../../shared_imports'; + +const mockReindexService = { + hasRequiredPrivileges: jest.fn(), + detectReindexWarnings: jest.fn(), + getIndexGroup: jest.fn(), + createReindexOperation: jest.fn(), + findAllInProgressOperations: jest.fn(), + findReindexOperation: jest.fn(), + processNextStep: jest.fn(), + resumeReindexOperation: jest.fn(), + cancelReindexing: jest.fn(), +}; +jest.mock('../../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +jest.mock('../../lib/reindexing', () => { + return { + reindexServiceFactory: () => mockReindexService, + }; +}); + +import { credentialStoreFactory } from '../../lib/reindexing/credential_store'; +import { registerBatchReindexIndicesRoutes } from './batch_reindex_indices'; + +const logMock = loggingSystemMock.create().get(); + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. Business logic is tested + * more thoroughly in the es_migration_apis test. + */ +describe('reindex API', () => { + let routeDependencies: any; + let mockRouter: MockRouter; + + const credentialStore = credentialStoreFactory(logMock); + const worker = { + includes: jest.fn(), + forceRefresh: jest.fn(), + } as any; + + beforeEach(() => { + mockRouter = createMockRouter(); + routeDependencies = { + credentialStore, + router: mockRouter, + licensing: licensingMock.createSetup(), + lib: { handleEsError }, + getSecurityPlugin: () => securityMock.createStart(), + }; + registerBatchReindexIndicesRoutes(routeDependencies, () => worker); + + mockReindexService.hasRequiredPrivileges.mockResolvedValue(true); + mockReindexService.detectReindexWarnings.mockReset(); + mockReindexService.getIndexGroup.mockReset(); + mockReindexService.createReindexOperation.mockReset(); + mockReindexService.findAllInProgressOperations.mockReset(); + mockReindexService.findReindexOperation.mockReset(); + mockReindexService.processNextStep.mockReset(); + mockReindexService.resumeReindexOperation.mockReset(); + mockReindexService.cancelReindexing.mockReset(); + worker.includes.mockReset(); + worker.forceRefresh.mockReset(); + + // Reset the credentialMap + credentialStore.clear(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /api/upgrade_assistant/reindex/batch', () => { + const queueSettingsArg = { + enqueue: true, + }; + it('creates a collection of index operations', async () => { + mockReindexService.createReindexOperation + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex1' }, + }) + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex2' }, + }) + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex3' }, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/batch', + })( + routeHandlerContextMock, + createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), + kibanaResponseFactory + ); + + // It called create correctly + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 1, + 'theIndex1', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 2, + 'theIndex2', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 3, + 'theIndex3', + queueSettingsArg + ); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data).toEqual({ + errors: [], + enqueued: [ + { indexName: 'theIndex1' }, + { indexName: 'theIndex2' }, + { indexName: 'theIndex3' }, + ], + }); + }); + + it('gracefully handles partial successes', async () => { + mockReindexService.createReindexOperation + .mockResolvedValueOnce({ + attributes: { indexName: 'theIndex1' }, + }) + .mockRejectedValueOnce(new Error('oops!')); + + mockReindexService.hasRequiredPrivileges + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/reindex/batch', + })( + routeHandlerContextMock, + createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), + kibanaResponseFactory + ); + + // It called create correctly + expect(mockReindexService.createReindexOperation).toHaveBeenCalledTimes(2); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 1, + 'theIndex1', + queueSettingsArg + ); + expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( + 2, + 'theIndex3', + queueSettingsArg + ); + + // It returned the right results + expect(resp.status).toEqual(200); + const data = resp.payload; + expect(data).toEqual({ + errors: [ + { + indexName: 'theIndex2', + message: 'You do not have adequate privileges to reindex "theIndex2".', + }, + { indexName: 'theIndex3', message: 'oops!' }, + ], + enqueued: [{ indexName: 'theIndex1' }], + }); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts new file mode 100644 index 0000000000000..62be9a1807aad --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/batch_reindex_indices.ts @@ -0,0 +1,133 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { ReindexStatus } from '../../../common/types'; +import { versionCheckHandlerWrapper } from '../../lib/es_version_precheck'; +import { ReindexWorker } from '../../lib/reindexing'; +import { reindexActionsFactory } from '../../lib/reindexing/reindex_actions'; +import { sortAndOrderReindexOperations } from '../../lib/reindexing/op_utils'; +import { RouteDependencies } from '../../types'; +import { mapAnyErrorToKibanaHttpResponse } from './map_any_error_to_kibana_http_response'; +import { reindexHandler } from './reindex_handler'; +import { GetBatchQueueResponse, PostBatchResponse } from './types'; + +export function registerBatchReindexIndicesRoutes( + { + credentialStore, + router, + licensing, + log, + getSecurityPlugin, + lib: { handleEsError }, + }: RouteDependencies, + getWorker: () => ReindexWorker +) { + const BASE_PATH = `${API_BASE_PATH}/reindex`; + + // Get the current batch queue + router.get( + { + path: `${BASE_PATH}/batch/queue`, + validate: {}, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client: esClient }, + savedObjects, + }, + }, + request, + response + ) => { + const { client } = savedObjects; + const callAsCurrentUser = esClient.asCurrentUser; + const reindexActions = reindexActionsFactory(client, callAsCurrentUser); + try { + const inProgressOps = await reindexActions.findAllByStatus(ReindexStatus.inProgress); + const { queue } = sortAndOrderReindexOperations(inProgressOps); + const result: GetBatchQueueResponse = { + queue: queue.map((savedObject) => savedObject.attributes), + }; + return response.ok({ + body: result, + }); + } catch (error) { + if (error instanceof errors.ResponseError) { + return handleEsError({ error, response }); + } + return mapAnyErrorToKibanaHttpResponse(error); + } + } + ) + ); + + // Add indices for reindexing to the worker's batch + router.post( + { + path: `${BASE_PATH}/batch`, + validate: { + body: schema.object({ + indexNames: schema.arrayOf(schema.string()), + }), + }, + }, + versionCheckHandlerWrapper( + async ( + { + core: { + savedObjects: { client: savedObjectsClient }, + elasticsearch: { client: esClient }, + }, + }, + request, + response + ) => { + const { indexNames } = request.body; + const results: PostBatchResponse = { + enqueued: [], + errors: [], + }; + for (const indexName of indexNames) { + try { + const result = await reindexHandler({ + savedObjects: savedObjectsClient, + dataClient: esClient, + indexName, + log, + licensing, + request, + credentialStore, + reindexOptions: { + enqueue: true, + }, + security: getSecurityPlugin(), + }); + results.enqueued.push(result); + } catch (e) { + results.errors.push({ + indexName, + message: e.message, + }); + } + } + + if (results.errors.length < indexNames.length) { + // Kick the worker on this node to immediately pickup the batch. + getWorker().forceRefresh(); + } + + return response.ok({ body: results }); + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts new file mode 100644 index 0000000000000..72d68fc132cb6 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ElasticsearchServiceStart, + Logger, + SavedObjectsClient, +} from '../../../../../../src/core/server'; + +import { LicensingPluginSetup } from '../../../../licensing/server'; +import { SecurityPluginStart } from '../../../../security/server'; +import { ReindexWorker } from '../../lib/reindexing'; +import { CredentialStore } from '../../lib/reindexing/credential_store'; + +interface CreateReindexWorker { + logger: Logger; + elasticsearchService: ElasticsearchServiceStart; + credentialStore: CredentialStore; + savedObjects: SavedObjectsClient; + licensing: LicensingPluginSetup; + security: SecurityPluginStart; +} + +export function createReindexWorker({ + logger, + elasticsearchService, + credentialStore, + savedObjects, + licensing, + security, +}: CreateReindexWorker) { + const esClient = elasticsearchService.client; + return new ReindexWorker(savedObjects, credentialStore, esClient, logger, licensing, security); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts index 97d8f495c16bb..038f0c07c11fe 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/index.ts @@ -5,4 +5,6 @@ * 2.0. */ -export { createReindexWorker, registerReindexIndicesRoutes } from './reindex_indices'; +export { createReindexWorker } from './create_reindex_worker'; +export { registerReindexIndicesRoutes } from './reindex_indices'; +export { registerBatchReindexIndicesRoutes } from './batch_reindex_indices'; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/map_any_error_to_kibana_http_response.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/map_any_error_to_kibana_http_response.ts new file mode 100644 index 0000000000000..f36e52ffb0eab --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/map_any_error_to_kibana_http_response.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 { kibanaResponseFactory } from '../../../../../../src/core/server'; + +import { + AccessForbidden, + CannotCreateIndex, + IndexNotFound, + MultipleReindexJobsFound, + ReindexAlreadyInProgress, + ReindexCannotBeCancelled, + ReindexTaskCannotBeDeleted, + ReindexTaskFailed, +} from '../../lib/reindexing/error_symbols'; +import { ReindexError } from '../../lib/reindexing/error'; + +export const mapAnyErrorToKibanaHttpResponse = (e: any) => { + if (e instanceof ReindexError) { + switch (e.symbol) { + case AccessForbidden: + return kibanaResponseFactory.forbidden({ body: e.message }); + case IndexNotFound: + return kibanaResponseFactory.notFound({ body: e.message }); + case CannotCreateIndex: + case ReindexTaskCannotBeDeleted: + throw e; + case ReindexTaskFailed: + // Bad data + return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); + case ReindexAlreadyInProgress: + case MultipleReindexJobsFound: + case ReindexCannotBeCancelled: + return kibanaResponseFactory.badRequest({ body: e.message }); + default: + // nothing matched + } + } + + throw e; +}; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts index fe9b95787b7d1..d81dc8cec4c53 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_handler.ts @@ -6,9 +6,15 @@ */ import { i18n } from '@kbn/i18n'; -import { IScopedClusterClient, Logger, SavedObjectsClientContract } from 'kibana/server'; +import { + IScopedClusterClient, + Logger, + SavedObjectsClientContract, + KibanaRequest, +} from 'kibana/server'; import { LicensingPluginSetup } from '../../../../licensing/server'; +import { SecurityPluginStart } from '../../../../security/server'; import { ReindexOperation, ReindexStatus } from '../../../common/types'; @@ -23,22 +29,24 @@ interface ReindexHandlerArgs { indexName: string; log: Logger; licensing: LicensingPluginSetup; - headers: Record; + request: KibanaRequest; credentialStore: CredentialStore; reindexOptions?: { enqueue?: boolean; }; + security?: SecurityPluginStart; } export const reindexHandler = async ({ credentialStore, dataClient, - headers, + request, indexName, licensing, log, savedObjects, reindexOptions, + security, }: ReindexHandlerArgs): Promise => { const callAsCurrentUser = dataClient.asCurrentUser; const reindexActions = reindexActionsFactory(savedObjects, callAsCurrentUser); @@ -62,7 +70,7 @@ export const reindexHandler = async ({ : await reindexService.createReindexOperation(indexName, reindexOptions); // Add users credentials for the worker to use - credentialStore.set(reindexOp, headers); + await credentialStore.set({ reindexOp, request, security }); return reindexOp.attributes; }; diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts index 08d9995ee6219..9fcff5748a987 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts @@ -6,14 +6,17 @@ */ import { kibanaResponseFactory } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { licensingMock } from '../../../../licensing/server/mocks'; +import { securityMock } from '../../../../security/server/mocks'; import { createMockRouter, MockRouter, routeHandlerContextMock } from '../__mocks__/routes.mock'; import { createRequestMock } from '../__mocks__/request.mock'; +import { handleEsError } from '../../shared_imports'; +import { errors as esErrors } from '@elastic/elasticsearch'; const mockReindexService = { hasRequiredPrivileges: jest.fn(), detectReindexWarnings: jest.fn(), - getIndexGroup: jest.fn(), createReindexOperation: jest.fn(), findAllInProgressOperations: jest.fn(), findReindexOperation: jest.fn(), @@ -31,10 +34,12 @@ jest.mock('../../lib/reindexing', () => { }; }); -import { IndexGroup, ReindexSavedObject, ReindexStatus } from '../../../common/types'; +import { ReindexSavedObject, ReindexStatus } from '../../../common/types'; import { credentialStoreFactory } from '../../lib/reindexing/credential_store'; import { registerReindexIndicesRoutes } from './reindex_indices'; +const logMock = loggingSystemMock.create().get(); + /** * Since these route callbacks are so thin, these serve simply as integration tests * to ensure they're wired up to the lib functions correctly. Business logic is tested @@ -44,7 +49,7 @@ describe('reindex API', () => { let routeDependencies: any; let mockRouter: MockRouter; - const credentialStore = credentialStoreFactory(); + const credentialStore = credentialStoreFactory(logMock); const worker = { includes: jest.fn(), forceRefresh: jest.fn(), @@ -56,12 +61,13 @@ describe('reindex API', () => { credentialStore, router: mockRouter, licensing: licensingMock.createSetup(), + lib: { handleEsError }, + getSecurityPlugin: () => securityMock.createStart(), }; registerReindexIndicesRoutes(routeDependencies, () => worker); mockReindexService.hasRequiredPrivileges.mockResolvedValue(true); mockReindexService.detectReindexWarnings.mockReset(); - mockReindexService.getIndexGroup.mockReset(); mockReindexService.createReindexOperation.mockReset(); mockReindexService.findAllInProgressOperations.mockReset(); mockReindexService.findReindexOperation.mockReset(); @@ -120,9 +126,11 @@ describe('reindex API', () => { ]); }); - it("returns null for both if reindex operation doesn't exist and index doesn't exist", async () => { + it('returns es errors', async () => { mockReindexService.findReindexOperation.mockResolvedValueOnce(null); - mockReindexService.detectReindexWarnings.mockResolvedValueOnce(null); + mockReindexService.detectReindexWarnings.mockRejectedValueOnce( + new esErrors.ResponseError({ statusCode: 404 } as any) + ); const resp = await routeDependencies.router.getHandler({ method: 'get', @@ -133,16 +141,12 @@ describe('reindex API', () => { kibanaResponseFactory ); - expect(resp.status).toEqual(200); - const data = resp.payload; - expect(data.reindexOp).toBeNull(); - expect(data.warnings).toBeNull(); + expect(resp.status).toEqual(404); }); - it('returns the indexGroup for ML indices', async () => { + it("returns null for both if reindex operation doesn't exist and index doesn't exist", async () => { mockReindexService.findReindexOperation.mockResolvedValueOnce(null); - mockReindexService.detectReindexWarnings.mockResolvedValueOnce([]); - mockReindexService.getIndexGroup.mockReturnValue(IndexGroup.ml); + mockReindexService.detectReindexWarnings.mockResolvedValueOnce(null); const resp = await routeDependencies.router.getHandler({ method: 'get', @@ -155,7 +159,8 @@ describe('reindex API', () => { expect(resp.status).toEqual(200); const data = resp.payload; - expect(data.indexGroup).toEqual(IndexGroup.ml); + expect(data.reindexOp).toBeNull(); + expect(data.warnings).toBeNull(); }); }); @@ -269,111 +274,6 @@ describe('reindex API', () => { }); }); - describe('POST /api/upgrade_assistant/reindex/batch', () => { - const queueSettingsArg = { - enqueue: true, - }; - it('creates a collection of index operations', async () => { - mockReindexService.createReindexOperation - .mockResolvedValueOnce({ - attributes: { indexName: 'theIndex1' }, - }) - .mockResolvedValueOnce({ - attributes: { indexName: 'theIndex2' }, - }) - .mockResolvedValueOnce({ - attributes: { indexName: 'theIndex3' }, - }); - - const resp = await routeDependencies.router.getHandler({ - method: 'post', - pathPattern: '/api/upgrade_assistant/reindex/batch', - })( - routeHandlerContextMock, - createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), - kibanaResponseFactory - ); - - // It called create correctly - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 1, - 'theIndex1', - queueSettingsArg - ); - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 2, - 'theIndex2', - queueSettingsArg - ); - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 3, - 'theIndex3', - queueSettingsArg - ); - - // It returned the right results - expect(resp.status).toEqual(200); - const data = resp.payload; - expect(data).toEqual({ - errors: [], - enqueued: [ - { indexName: 'theIndex1' }, - { indexName: 'theIndex2' }, - { indexName: 'theIndex3' }, - ], - }); - }); - - it('gracefully handles partial successes', async () => { - mockReindexService.createReindexOperation - .mockResolvedValueOnce({ - attributes: { indexName: 'theIndex1' }, - }) - .mockRejectedValueOnce(new Error('oops!')); - - mockReindexService.hasRequiredPrivileges - .mockResolvedValueOnce(true) - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(true); - - const resp = await routeDependencies.router.getHandler({ - method: 'post', - pathPattern: '/api/upgrade_assistant/reindex/batch', - })( - routeHandlerContextMock, - createRequestMock({ body: { indexNames: ['theIndex1', 'theIndex2', 'theIndex3'] } }), - kibanaResponseFactory - ); - - // It called create correctly - expect(mockReindexService.createReindexOperation).toHaveBeenCalledTimes(2); - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 1, - 'theIndex1', - queueSettingsArg - ); - expect(mockReindexService.createReindexOperation).toHaveBeenNthCalledWith( - 2, - 'theIndex3', - queueSettingsArg - ); - - // It returned the right results - expect(resp.status).toEqual(200); - const data = resp.payload; - expect(data).toEqual({ - errors: [ - { - indexName: 'theIndex2', - message: 'You do not have adequate privileges to reindex "theIndex2".', - }, - { indexName: 'theIndex3', message: 'oops!' }, - ], - enqueued: [{ indexName: 'theIndex1' }], - }); - }); - }); - describe('POST /api/upgrade_assistant/reindex/{indexName}/cancel', () => { it('returns a 501', async () => { mockReindexService.cancelReindexing.mockResolvedValueOnce({}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts index 5528c0847822a..30f7c77cf73ab 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.ts @@ -6,84 +6,25 @@ */ import { schema } from '@kbn/config-schema'; -import { API_BASE_PATH } from '../../../common/constants'; -import { - ElasticsearchServiceStart, - kibanaResponseFactory, - Logger, - SavedObjectsClient, -} from '../../../../../../src/core/server'; - -import { LicensingPluginSetup } from '../../../../licensing/server'; - -import { ReindexStatus } from '../../../common/types'; +import { errors } from '@elastic/elasticsearch'; +import { API_BASE_PATH } from '../../../common/constants'; import { versionCheckHandlerWrapper } from '../../lib/es_version_precheck'; import { reindexServiceFactory, ReindexWorker } from '../../lib/reindexing'; -import { CredentialStore } from '../../lib/reindexing/credential_store'; import { reindexActionsFactory } from '../../lib/reindexing/reindex_actions'; -import { sortAndOrderReindexOperations } from '../../lib/reindexing/op_utils'; -import { ReindexError } from '../../lib/reindexing/error'; import { RouteDependencies } from '../../types'; -import { - AccessForbidden, - CannotCreateIndex, - IndexNotFound, - MultipleReindexJobsFound, - ReindexAlreadyInProgress, - ReindexCannotBeCancelled, - ReindexTaskCannotBeDeleted, - ReindexTaskFailed, -} from '../../lib/reindexing/error_symbols'; - +import { mapAnyErrorToKibanaHttpResponse } from './map_any_error_to_kibana_http_response'; import { reindexHandler } from './reindex_handler'; -import { GetBatchQueueResponse, PostBatchResponse } from './types'; - -interface CreateReindexWorker { - logger: Logger; - elasticsearchService: ElasticsearchServiceStart; - credentialStore: CredentialStore; - savedObjects: SavedObjectsClient; - licensing: LicensingPluginSetup; -} - -export function createReindexWorker({ - logger, - elasticsearchService, - credentialStore, - savedObjects, - licensing, -}: CreateReindexWorker) { - const esClient = elasticsearchService.client; - return new ReindexWorker(savedObjects, credentialStore, esClient, logger, licensing); -} - -const mapAnyErrorToKibanaHttpResponse = (e: any) => { - if (e instanceof ReindexError) { - switch (e.symbol) { - case AccessForbidden: - return kibanaResponseFactory.forbidden({ body: e.message }); - case IndexNotFound: - return kibanaResponseFactory.notFound({ body: e.message }); - case CannotCreateIndex: - case ReindexTaskCannotBeDeleted: - throw e; - case ReindexTaskFailed: - // Bad data - return kibanaResponseFactory.customError({ body: e.message, statusCode: 422 }); - case ReindexAlreadyInProgress: - case MultipleReindexJobsFound: - case ReindexCannotBeCancelled: - return kibanaResponseFactory.badRequest({ body: e.message }); - default: - // nothing matched - } - } - throw e; -}; export function registerReindexIndicesRoutes( - { credentialStore, router, licensing, log }: RouteDependencies, + { + credentialStore, + router, + licensing, + log, + getSecurityPlugin, + lib: { handleEsError }, + }: RouteDependencies, getWorker: () => ReindexWorker ) { const BASE_PATH = `${API_BASE_PATH}/reindex`; @@ -117,8 +58,9 @@ export function registerReindexIndicesRoutes( indexName, log, licensing, - headers: request.headers, + request, credentialStore, + security: getSecurityPlugin(), }); // Kick the worker on this node to immediately pickup the new reindex operation. @@ -127,102 +69,12 @@ export function registerReindexIndicesRoutes( return response.ok({ body: result, }); - } catch (e) { - return mapAnyErrorToKibanaHttpResponse(e); - } - } - ) - ); - - // Get the current batch queue - router.get( - { - path: `${BASE_PATH}/batch/queue`, - validate: {}, - }, - async ( - { - core: { - elasticsearch: { client: esClient }, - savedObjects, - }, - }, - request, - response - ) => { - const { client } = savedObjects; - const callAsCurrentUser = esClient.asCurrentUser; - const reindexActions = reindexActionsFactory(client, callAsCurrentUser); - try { - const inProgressOps = await reindexActions.findAllByStatus(ReindexStatus.inProgress); - const { queue } = sortAndOrderReindexOperations(inProgressOps); - const result: GetBatchQueueResponse = { - queue: queue.map((savedObject) => savedObject.attributes), - }; - return response.ok({ - body: result, - }); - } catch (e) { - return mapAnyErrorToKibanaHttpResponse(e); - } - } - ); - - // Add indices for reindexing to the worker's batch - router.post( - { - path: `${BASE_PATH}/batch`, - validate: { - body: schema.object({ - indexNames: schema.arrayOf(schema.string()), - }), - }, - }, - versionCheckHandlerWrapper( - async ( - { - core: { - savedObjects: { client: savedObjectsClient }, - elasticsearch: { client: esClient }, - }, - }, - request, - response - ) => { - const { indexNames } = request.body; - const results: PostBatchResponse = { - enqueued: [], - errors: [], - }; - for (const indexName of indexNames) { - try { - const result = await reindexHandler({ - savedObjects: savedObjectsClient, - dataClient: esClient, - indexName, - log, - licensing, - headers: request.headers, - credentialStore, - reindexOptions: { - enqueue: true, - }, - }); - results.enqueued.push(result); - } catch (e) { - results.errors.push({ - indexName, - message: e.message, - }); + } catch (error) { + if (error instanceof errors.ResponseError) { + return handleEsError({ error, response }); } + return mapAnyErrorToKibanaHttpResponse(error); } - - if (results.errors.length < indexNames.length) { - // Kick the worker on this node to immediately pickup the batch. - getWorker().forceRefresh(); - } - - return response.ok({ body: results }); } ) ); @@ -261,18 +113,19 @@ export function registerReindexIndicesRoutes( const warnings = hasRequiredPrivileges ? await reindexService.detectReindexWarnings(indexName) : []; - const indexGroup = reindexService.getIndexGroup(indexName); return response.ok({ body: { reindexOp: reindexOp ? reindexOp.attributes : null, warnings, - indexGroup, hasRequiredPrivileges, }, }); - } catch (e) { - return mapAnyErrorToKibanaHttpResponse(e); + } catch (error) { + if (error instanceof errors.ResponseError) { + return handleEsError({ error, response }); + } + return mapAnyErrorToKibanaHttpResponse(error); } } ) @@ -314,8 +167,12 @@ export function registerReindexIndicesRoutes( await reindexService.cancelReindexing(indexName); return response.ok({ body: { acknowledged: true } }); - } catch (e) { - return mapAnyErrorToKibanaHttpResponse(e); + } catch (error) { + if (error instanceof errors.ResponseError) { + return handleEsError({ error, response }); + } + + return mapAnyErrorToKibanaHttpResponse(error); } } ) diff --git a/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts index bd5299ad8a4f3..e442d3b4fd11c 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/status.test.ts @@ -6,6 +6,8 @@ */ import { kibanaResponseFactory } from 'src/core/server'; + +import { handleEsError } from '../shared_imports'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; import { registerUpgradeStatusRoute } from './status'; @@ -31,6 +33,7 @@ describe('Status API', () => { mockRouter = createMockRouter(); routeDependencies = { router: mockRouter, + lib: { handleEsError }, }; registerUpgradeStatusRoute(routeDependencies); }); @@ -74,7 +77,7 @@ describe('Status API', () => { expect(resp.payload).toEqual({ readyForUpgrade: false, details: - 'You have 1 Elasticsearch deprecation issues and 1 Kibana deprecation issues that must be resolved before upgrading.', + 'You have 1 Elasticsearch deprecation issue and 1 Kibana deprecation issue that must be resolved before upgrading.', }); }); @@ -97,7 +100,7 @@ describe('Status API', () => { expect(resp.status).toEqual(200); expect(resp.payload).toEqual({ readyForUpgrade: true, - details: 'All deprecation issues have been resolved.', + details: 'All deprecation warnings have been resolved.', }); }); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/status.ts b/x-pack/plugins/upgrade_assistant/server/routes/status.ts index 1e0a0060de030..ce9bb2e1c55d0 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/status.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/status.ts @@ -11,9 +11,11 @@ import { getESUpgradeStatus } from '../lib/es_deprecations_status'; import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; import { getKibanaUpgradeStatus } from '../lib/kibana_status'; import { RouteDependencies } from '../types'; -import { handleEsError } from '../shared_imports'; -export function registerUpgradeStatusRoute({ router }: RouteDependencies) { +/** + * Note that this route is primarily intended for consumption by Cloud. + */ +export function registerUpgradeStatusRoute({ router, lib: { handleEsError } }: RouteDependencies) { router.get( { path: `${API_BASE_PATH}/status`, @@ -45,14 +47,14 @@ export function registerUpgradeStatusRoute({ router }: RouteDependencies) { return i18n.translate( 'xpack.upgradeAssistant.status.allDeprecationsResolvedMessage', { - defaultMessage: 'All deprecation issues have been resolved.', + defaultMessage: 'All deprecation warnings have been resolved.', } ); } return i18n.translate('xpack.upgradeAssistant.status.deprecationsUnresolvedMessage', { defaultMessage: - 'You have {esTotalCriticalDeps} Elasticsearch deprecation issues and {kibanaTotalCriticalDeps} Kibana deprecation issues that must be resolved before upgrading.', + 'You have {esTotalCriticalDeps} Elasticsearch deprecation {esTotalCriticalDeps, plural, one {issue} other {issues}} and {kibanaTotalCriticalDeps} Kibana deprecation {kibanaTotalCriticalDeps, plural, one {issue} other {issues}} that must be resolved before upgrading.', values: { esTotalCriticalDeps, kibanaTotalCriticalDeps }, }); }; @@ -63,8 +65,8 @@ export function registerUpgradeStatusRoute({ router }: RouteDependencies) { details: getStatusMessage(), }, }); - } catch (e) { - return handleEsError({ error: e, response }); + } catch (error) { + return handleEsError({ error, response }); } } ) diff --git a/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts new file mode 100644 index 0000000000000..910748661ac41 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.test.ts @@ -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 { kibanaResponseFactory } from 'src/core/server'; +import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; +import { createRequestMock } from './__mocks__/request.mock'; +import { handleEsError } from '../shared_imports'; + +jest.mock('../lib/es_version_precheck', () => ({ + versionCheckHandlerWrapper: (a: any) => a, +})); + +import { registerSystemIndicesMigrationRoutes } from './system_indices_migration'; + +const mockedResponse = { + features: [ + { + feature_name: 'security', + minimum_index_version: '7.1.1', + migration_status: 'NO_MIGRATION_NEEDED', + indices: [ + { + index: '.security-7', + version: '7.1.1', + }, + ], + }, + { + feature_name: 'kibana', + minimum_index_version: '7.1.2', + upgrade_status: 'MIGRATION_NEEDED', + indices: [ + { + index: '.kibana', + version: '7.1.2', + }, + ], + }, + ], + migration_status: 'MIGRATION_NEEDED', +}; + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the lib functions correctly. + */ +describe('Migrate system indices API', () => { + let mockRouter: MockRouter; + let routeDependencies: any; + + beforeEach(() => { + mockRouter = createMockRouter(); + routeDependencies = { + router: mockRouter, + lib: { handleEsError }, + }; + registerSystemIndicesMigrationRoutes(routeDependencies); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /api/upgrade_assistant/system_indices_migration', () => { + it('returns system indices migration status', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockResolvedValue({ + body: mockedResponse, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'GET', + path: '/_migration/system_features', + }); + expect(resp.payload).toEqual({ + ...mockedResponse, + features: mockedResponse.features.filter( + (feature) => feature.migration_status !== 'NO_MIGRATION_NEEDED' + ), + }); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'get', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); + + describe('POST /api/upgrade_assistant/system_indices_migration', () => { + it('returns system indices migration status', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockResolvedValue({ + body: mockedResponse, + }); + + const resp = await routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory); + + expect(resp.status).toEqual(200); + expect( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'POST', + path: '/_migration/system_features', + }); + expect(resp.payload).toEqual(mockedResponse); + }); + + it('returns an error if it throws', async () => { + ( + routeHandlerContextMock.core.elasticsearch.client.asCurrentUser.transport + .request as jest.Mock + ).mockRejectedValue(new Error('scary error!')); + await expect( + routeDependencies.router.getHandler({ + method: 'post', + pathPattern: '/api/upgrade_assistant/system_indices_migration', + })(routeHandlerContextMock, createRequestMock(), kibanaResponseFactory) + ).rejects.toThrow('scary error!'); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts new file mode 100644 index 0000000000000..67f91aa08a076 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/routes/system_indices_migration.ts @@ -0,0 +1,76 @@ +/* + * 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 { API_BASE_PATH } from '../../common/constants'; +import { versionCheckHandlerWrapper } from '../lib/es_version_precheck'; +import { RouteDependencies } from '../types'; +import { + getESSystemIndicesMigrationStatus, + startESSystemIndicesMigration, +} from '../lib/es_system_indices_migration'; + +export function registerSystemIndicesMigrationRoutes({ + router, + lib: { handleEsError }, +}: RouteDependencies) { + // GET status of the system indices migration + router.get( + { path: `${API_BASE_PATH}/system_indices_migration`, validate: false }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const status = await getESSystemIndicesMigrationStatus(client.asCurrentUser); + + return response.ok({ + body: { + ...status, + features: status.features.filter( + (feature) => feature.migration_status !== 'NO_MIGRATION_NEEDED' + ), + }, + }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); + + // POST starts the system indices migration + router.post( + { path: `${API_BASE_PATH}/system_indices_migration`, validate: false }, + versionCheckHandlerWrapper( + async ( + { + core: { + elasticsearch: { client }, + }, + }, + request, + response + ) => { + try { + const status = await startESSystemIndicesMigration(client.asCurrentUser); + + return response.ok({ + body: status, + }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ) + ); +} diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts deleted file mode 100644 index 578cceb702751..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ /dev/null @@ -1,187 +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 { kibanaResponseFactory } from 'src/core/server'; -import { savedObjectsServiceMock } from 'src/core/server/mocks'; -import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; -import { createRequestMock } from './__mocks__/request.mock'; - -jest.mock('../lib/telemetry/es_ui_open_apis', () => ({ - upsertUIOpenOption: jest.fn(), -})); - -jest.mock('../lib/telemetry/es_ui_reindex_apis', () => ({ - upsertUIReindexOption: jest.fn(), -})); - -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { registerTelemetryRoutes } from './telemetry'; - -/** - * Since these route callbacks are so thin, these serve simply as integration tests - * to ensure they're wired up to the lib functions correctly. Business logic is tested - * more thoroughly in the lib/telemetry tests. - */ -describe('Upgrade Assistant Telemetry API', () => { - let routeDependencies: any; - let mockRouter: MockRouter; - beforeEach(() => { - mockRouter = createMockRouter(); - routeDependencies = { - getSavedObjectsService: () => savedObjectsServiceMock.create(), - router: mockRouter, - }; - registerTelemetryRoutes(routeDependencies); - }); - afterEach(() => jest.clearAllMocks()); - - describe('PUT /api/upgrade_assistant/stats/ui_open', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - overview: true, - elasticsearch: false, - kibana: false, - }; - - (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ body: returnPayload }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - overview: true, - elasticsearch: true, - kibana: true, - }; - - (upsertUIOpenOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: true, - elasticsearch: true, - kibana: true, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - (upsertUIOpenOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - - await expect( - routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_open', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ) - ).rejects.toThrowError('scary error!'); - }); - }); - - describe('PUT /api/upgrade_assistant/stats/ui_reindex', () => { - it('returns correct payload with single option', async () => { - const returnPayload = { - close: false, - open: false, - start: true, - stop: false, - }; - - (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - overview: false, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns correct payload with multiple option', async () => { - const returnPayload = { - close: true, - open: true, - start: true, - stop: true, - }; - - (upsertUIReindexOption as jest.Mock).mockResolvedValue(returnPayload); - - const resp = await routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - close: true, - open: true, - start: true, - stop: true, - }, - }), - kibanaResponseFactory - ); - - expect(resp.payload).toEqual(returnPayload); - }); - - it('returns an error if it throws', async () => { - (upsertUIReindexOption as jest.Mock).mockRejectedValue(new Error(`scary error!`)); - - await expect( - routeDependencies.router.getHandler({ - method: 'put', - pathPattern: '/api/upgrade_assistant/stats/ui_reindex', - })( - routeHandlerContextMock, - createRequestMock({ - body: { - start: false, - }, - }), - kibanaResponseFactory - ) - ).rejects.toThrowError('scary error!'); - }); - }); -}); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts deleted file mode 100644 index d083b38c7c240..0000000000000 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ /dev/null @@ -1,64 +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 { schema } from '@kbn/config-schema'; -import { API_BASE_PATH } from '../../common/constants'; -import { upsertUIOpenOption } from '../lib/telemetry/es_ui_open_apis'; -import { upsertUIReindexOption } from '../lib/telemetry/es_ui_reindex_apis'; -import { RouteDependencies } from '../types'; - -export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { - router.put( - { - path: `${API_BASE_PATH}/stats/ui_open`, - validate: { - body: schema.object({ - overview: schema.boolean({ defaultValue: false }), - elasticsearch: schema.boolean({ defaultValue: false }), - kibana: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (ctx, request, response) => { - const { elasticsearch, overview, kibana } = request.body; - return response.ok({ - body: await upsertUIOpenOption({ - savedObjects: getSavedObjectsService(), - elasticsearch, - overview, - kibana, - }), - }); - } - ); - - router.put( - { - path: `${API_BASE_PATH}/stats/ui_reindex`, - validate: { - body: schema.object({ - close: schema.boolean({ defaultValue: false }), - open: schema.boolean({ defaultValue: false }), - start: schema.boolean({ defaultValue: false }), - stop: schema.boolean({ defaultValue: false }), - }), - }, - }, - async (ctx, request, response) => { - const { close, open, start, stop } = request.body; - return response.ok({ - body: await upsertUIReindexOption({ - savedObjects: getSavedObjectsService(), - close, - open, - start, - stop, - }), - }); - } - ); -} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/index.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/index.ts similarity index 74% rename from x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/index.ts rename to x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/index.ts index 185ec5f2540c4..5e6e379bd9b2b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/review_logs_step/kibana_stats/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { KibanaDeprecationStats } from './kibana_stats'; +export { telemetrySavedObjectMigrations } from './telemetry_saved_object_migrations'; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.test.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.test.ts new file mode 100644 index 0000000000000..e1250ee0ebfe0 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.test.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 { telemetrySavedObjectMigrations } from './telemetry_saved_object_migrations'; + +describe('Telemetry saved object migration', () => { + describe('7.16.0', () => { + test('removes ui_open and ui_reindex attributes while preserving other attributes', () => { + const doc = { + type: 'upgrade-assistant-telemetry', + id: 'upgrade-assistant-telemetry', + attributes: { + 'test.property': 5, + 'ui_open.cluster': 1, + 'ui_open.indices': 1, + 'ui_open.overview': 1, + 'ui_reindex.close': 1, + 'ui_reindex.open': 1, + 'ui_reindex.start': 1, + 'ui_reindex.stop': 1, + }, + references: [], + updated_at: '2021-09-29T21:17:17.410Z', + migrationVersion: {}, + }; + + expect(telemetrySavedObjectMigrations['7.16.0'](doc)).toStrictEqual({ + type: 'upgrade-assistant-telemetry', + id: 'upgrade-assistant-telemetry', + attributes: { 'test.property': 5 }, + references: [], + updated_at: '2021-09-29T21:17:17.410Z', + migrationVersion: {}, + }); + }); + }); +}); diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.ts new file mode 100644 index 0000000000000..88540d67b13df --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/migrations/telemetry_saved_object_migrations.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get, omit, flow, some } from 'lodash'; +import type { SavedObjectMigrationFn } from 'kibana/server'; + +const v716RemoveUnusedTelemetry: SavedObjectMigrationFn = (doc) => { + // Dynamically defined in 6.7 (https://github.com/elastic/kibana/pull/28878) + // and then statically defined in 7.8 (https://github.com/elastic/kibana/pull/64332). + const attributesBlocklist = [ + 'ui_open.cluster', + 'ui_open.indices', + 'ui_open.overview', + 'ui_reindex.close', + 'ui_reindex.open', + 'ui_reindex.start', + 'ui_reindex.stop', + ]; + + const isDocEligible = some(attributesBlocklist, (attribute: string) => { + return get(doc, 'attributes', attribute); + }); + + if (isDocEligible) { + return { + ...doc, + attributes: omit(doc.attributes, attributesBlocklist), + }; + } + + return doc; +}; + +export const telemetrySavedObjectMigrations = { + '7.16.0': flow(v716RemoveUnusedTelemetry), +}; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts index 42d5d339dd050..43cf6c30fccab 100644 --- a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts @@ -7,50 +7,15 @@ import { SavedObjectsType } from 'src/core/server'; -import { UPGRADE_ASSISTANT_TYPE } from '../../common/types'; +import { UPGRADE_ASSISTANT_TELEMETRY } from '../../common/constants'; +import { telemetrySavedObjectMigrations } from './migrations'; export const telemetrySavedObjectType: SavedObjectsType = { - name: UPGRADE_ASSISTANT_TYPE, + name: UPGRADE_ASSISTANT_TELEMETRY, hidden: false, namespaceType: 'agnostic', mappings: { properties: { - ui_open: { - properties: { - overview: { - type: 'long', - null_value: 0, - }, - elasticsearch: { - type: 'long', - null_value: 0, - }, - kibana: { - type: 'long', - null_value: 0, - }, - }, - }, - ui_reindex: { - properties: { - close: { - type: 'long', - null_value: 0, - }, - open: { - type: 'long', - null_value: 0, - }, - start: { - type: 'long', - null_value: 0, - }, - stop: { - type: 'long', - null_value: 0, - }, - }, - }, features: { properties: { deprecation_logging: { @@ -65,4 +30,5 @@ export const telemetrySavedObjectType: SavedObjectsType = { }, }, }, + migrations: telemetrySavedObjectMigrations, }; diff --git a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts index 7f55d189457c7..1c43f89469ac1 100644 --- a/x-pack/plugins/upgrade_assistant/server/shared_imports.ts +++ b/x-pack/plugins/upgrade_assistant/server/shared_imports.ts @@ -6,3 +6,4 @@ */ export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +export type { Privileges } from '../../../../src/plugins/es_ui_shared/common'; diff --git a/x-pack/plugins/upgrade_assistant/server/types.ts b/x-pack/plugins/upgrade_assistant/server/types.ts index b25b73070e4cf..376514c59d494 100644 --- a/x-pack/plugins/upgrade_assistant/server/types.ts +++ b/x-pack/plugins/upgrade_assistant/server/types.ts @@ -6,13 +6,22 @@ */ import { IRouter, Logger, SavedObjectsServiceStart } from 'src/core/server'; -import { CredentialStore } from './lib/reindexing/credential_store'; import { LicensingPluginSetup } from '../../licensing/server'; +import { SecurityPluginStart } from '../../security/server'; +import { CredentialStore } from './lib/reindexing/credential_store'; +import { handleEsError } from './shared_imports'; export interface RouteDependencies { router: IRouter; credentialStore: CredentialStore; log: Logger; getSavedObjectsService: () => SavedObjectsServiceStart; + getSecurityPlugin: () => SecurityPluginStart | undefined; licensing: LicensingPluginSetup; + lib: { + handleEsError: typeof handleEsError; + }; + config: { + isSecurityEnabled: () => boolean; + }; } diff --git a/x-pack/plugins/upgrade_assistant/tsconfig.json b/x-pack/plugins/upgrade_assistant/tsconfig.json index 39d7404ebea9d..4336acb77c2eb 100644 --- a/x-pack/plugins/upgrade_assistant/tsconfig.json +++ b/x-pack/plugins/upgrade_assistant/tsconfig.json @@ -7,6 +7,7 @@ "declarationMap": true }, "include": [ + "../../../typings/**/*", "__jest__/**/*", "common/**/*", "public/**/*", diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/upgrade_assistant.ts index 1674948fef32e..2c0e81a6fb831 100644 --- a/x-pack/test/accessibility/apps/upgrade_assistant.ts +++ b/x-pack/test/accessibility/apps/upgrade_assistant.ts @@ -5,80 +5,199 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FtrProviderContext } from '../ftr_provider_context'; +const translogSettingsIndexDeprecation: estypes.IndicesCreateRequest = { + index: 'deprecated_settings', + body: { + settings: { + 'translog.retention.size': '1b', + 'translog.retention.age': '5m', + 'index.soft_deletes.enabled': true, + }, + }, +}; + +const multiFieldsIndexDeprecation: estypes.IndicesCreateRequest = { + index: 'nested_multi_fields', + body: { + mappings: { + properties: { + text: { + type: 'text', + fields: { + english: { + type: 'text', + analyzer: 'english', + fields: { + english: { + type: 'text', + analyzer: 'english', + }, + }, + }, + }, + }, + }, + }, + }, +}; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['upgradeAssistant', 'common']); const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const es = getService('es'); + const log = getService('log'); describe.skip('Upgrade Assistant', () => { before(async () => { await PageObjects.upgradeAssistant.navigateToPage(); + try { + // Create two indices that will trigger deprecation warnings to test the ES deprecations page + await es.indices.create(multiFieldsIndexDeprecation); + await es.indices.create(translogSettingsIndexDeprecation); + } catch (e) { + log.debug('[Setup error] Error creating indices'); + throw e; + } }); - // These tests will be skipped until the last minor of the next major release - describe('Upgrade Assistant content', () => { - it('Overview page', async () => { - await retry.waitFor('Upgrade Assistant overview page to be visible', async () => { - return testSubjects.exists('overviewPageContent'); + after(async () => { + try { + await es.indices.delete({ + index: [multiFieldsIndexDeprecation.index, translogSettingsIndexDeprecation.index], }); - await a11y.testAppSnapshot(); + } catch (e) { + log.debug('[Cleanup error] Error deleting indices'); + throw e; + } + }); + + describe('Upgrade Assistant - Overview', () => { + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + + try { + // Create two indices that will trigger deprecation warnings to test the ES deprecations page + await es.indices.create(multiFieldsIndexDeprecation); + await es.indices.create(translogSettingsIndexDeprecation); + } catch (e) { + log.debug('[Setup error] Error creating indices'); + throw e; + } + }); + + after(async () => { + try { + await es.indices.delete({ + index: [multiFieldsIndexDeprecation.index, translogSettingsIndexDeprecation.index], + }); + } catch (e) { + log.debug('[Cleanup error] Error deleting indices'); + throw e; + } }); - it('Elasticsearch cluster deprecations', async () => { - await PageObjects.common.navigateToUrl( - 'management', - 'stack/upgrade_assistant/es_deprecations/cluster', - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - } - ); - - await retry.waitFor('Cluster tab to be visible', async () => { - return testSubjects.exists('clusterTabContent'); + describe('Overview page', () => { + beforeEach(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + await retry.waitFor('Upgrade Assistant overview page to be visible', async () => { + return testSubjects.exists('overview'); + }); + }); + + it('with logs collection disabled', async () => { + await a11y.testAppSnapshot(); }); - await a11y.testAppSnapshot(); + it('with logs collection enabled', async () => { + await PageObjects.upgradeAssistant.clickDeprecationLoggingToggle(); + + await retry.waitFor('UA external links title to be present', async () => { + return testSubjects.isDisplayed('externalLinksTitle'); + }); + + await a11y.testAppSnapshot(); + }); }); - it('Elasticsearch index deprecations', async () => { - await PageObjects.common.navigateToUrl( - 'management', - 'stack/upgrade_assistant/es_deprecations/indices', - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - } - ); - - await retry.waitFor('Indices tab to be visible', async () => { - return testSubjects.exists('indexTabContent'); + describe('Elasticsearch deprecations page', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrl( + 'management', + 'stack/upgrade_assistant/es_deprecations', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + } + ); + + await retry.waitFor('Elasticsearch deprecations table to be visible', async () => { + return testSubjects.exists('esDeprecationsTable'); + }); + }); + + it('Deprecations table', async () => { + await a11y.testAppSnapshot(); }); - await a11y.testAppSnapshot(); + it('Index settings deprecation flyout', async () => { + await PageObjects.upgradeAssistant.clickEsDeprecation( + 'indexSettings' // An index setting deprecation was added in the before() hook so should be guaranteed + ); + await retry.waitFor('ES index settings deprecation flyout to be visible', async () => { + return testSubjects.exists('indexSettingsDetails'); + }); + await a11y.testAppSnapshot(); + }); + + it('Default deprecation flyout', async () => { + await PageObjects.upgradeAssistant.clickEsDeprecation( + 'default' // A default deprecation was added in the before() hook so should be guaranteed + ); + await retry.waitFor('ES default deprecation flyout to be visible', async () => { + return testSubjects.exists('defaultDeprecationDetails'); + }); + await a11y.testAppSnapshot(); + }); }); - it('Kibana deprecations', async () => { - await PageObjects.common.navigateToUrl( - 'management', - 'stack/upgrade_assistant/kibana_deprecations', - { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - } - ); - - await retry.waitFor('Kibana deprecations to be visible', async () => { - return testSubjects.exists('kibanaDeprecationsContent'); + describe('Kibana deprecations page', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrl( + 'management', + 'stack/upgrade_assistant/kibana_deprecations', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, + } + ); + + await retry.waitFor('Kibana deprecations to be visible', async () => { + return testSubjects.exists('kibanaDeprecations'); + }); + }); + + it('Deprecations table', async () => { + await a11y.testAppSnapshot(); }); - await a11y.testAppSnapshot(); + it('Deprecation details flyout', async () => { + await PageObjects.upgradeAssistant.clickKibanaDeprecation( + 'xpack.securitySolution has a deprecated setting' // This deprecation was added to the test runner config so should be guaranteed + ); + + await retry.waitFor('Kibana deprecation details flyout to be visible', async () => { + return testSubjects.exists('kibanaDeprecationDetails'); + }); + + await a11y.testAppSnapshot(); + }); }); }); }); diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts b/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts new file mode 100644 index 0000000000000..b1a4d7e8b0475 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + + const CLOUD_SNAPSHOT_REPOSITORY = 'found-snapshots'; + + const createCloudRepository = () => { + return es.snapshot.createRepository({ + name: CLOUD_SNAPSHOT_REPOSITORY, + body: { + type: 'fs', + settings: { + location: '/tmp/cloud-snapshots/', + }, + }, + verify: false, + }); + }; + + const createCloudSnapshot = (snapshotName: string) => { + return es.snapshot.create({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: snapshotName, + wait_for_completion: true, + // Configure snapshot so no indices are captured, so the request completes ASAP. + body: { + indices: 'this_index_doesnt_exist', + ignore_unavailable: true, + include_global_state: false, + }, + }); + }; + + const deleteCloudSnapshot = (snapshotName: string) => { + return es.snapshot.delete({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: snapshotName, + }); + }; + + describe('Cloud backup status', () => { + describe('get', () => { + describe('with backups present', () => { + // Needs SnapshotInfo type https://github.com/elastic/elasticsearch-specification/issues/685 + let mostRecentSnapshot: any; + + before(async () => { + await createCloudRepository(); + await createCloudSnapshot('test_snapshot_1'); + mostRecentSnapshot = (await createCloudSnapshot('test_snapshot_2')).snapshot; + }); + + after(async () => { + await deleteCloudSnapshot('test_snapshot_1'); + await deleteCloudSnapshot('test_snapshot_2'); + }); + + it('returns status based on most recent snapshot', async () => { + const { body: cloudBackupStatus } = await supertest + .get('/api/upgrade_assistant/cloud_backup_status') + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(cloudBackupStatus.isBackedUp).to.be(true); + expect(cloudBackupStatus.lastBackupTime).to.be(mostRecentSnapshot.start_time); + }); + }); + + describe('without backups present', () => { + it('returns not-backed-up status', async () => { + const { body: cloudBackupStatus } = await supertest + .get('/api/upgrade_assistant/cloud_backup_status') + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(cloudBackupStatus.isBackedUp).to.be(false); + expect(cloudBackupStatus.lastBackupTime).to.be(undefined); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts b/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.ts new file mode 100644 index 0000000000000..aea003a317963 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/es_deprecations.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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const security = getService('security'); + + describe('Elasticsearch deprecations', () => { + describe('GET /api/upgrade_assistant/es_deprecations', () => { + it('handles auth error', async () => { + const ROLE_NAME = 'authErrorRole'; + const USER_NAME = 'authErrorUser'; + const USER_PASSWORD = 'password'; + + try { + await security.role.create(ROLE_NAME, {}); + await security.user.create(USER_NAME, { + password: USER_PASSWORD, + roles: [ROLE_NAME], + }); + + await supertestWithoutAuth + .get('/api/upgrade_assistant/es_deprecations') + .auth(USER_NAME, USER_PASSWORD) + .set('kbn-xsrf', 'kibana') + .send() + .expect(403); + } finally { + await security.role.delete(ROLE_NAME); + await security.user.delete(USER_NAME); + } + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts index 466d44ca460ac..f6b231f038817 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts @@ -10,5 +10,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Upgrade Assistant', () => { loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./cloud_backup_status')); + loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./es_deprecations')); }); } diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts b/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts new file mode 100644 index 0000000000000..c5c00c9a33685 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/privileges.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { DEPRECATION_LOGS_INDEX } from '../../../../plugins/upgrade_assistant/common/constants'; + +export default function ({ getService }: FtrProviderContext) { + const security = getService('security'); + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('Privileges', () => { + describe('GET /api/upgrade_assistant/privileges', () => { + it('User with with index privileges', async () => { + const { body } = await supertest + .get('/api/upgrade_assistant/privileges') + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(body.hasAllPrivileges).to.be(true); + expect(body.missingPrivileges.index.length).to.be(0); + }); + + it('User without index privileges', async () => { + const ROLE_NAME = 'test_role'; + const USER_NAME = 'test_user'; + const USER_PASSWORD = 'test_user'; + + try { + await security.role.create(ROLE_NAME, {}); + await security.user.create(USER_NAME, { + password: USER_PASSWORD, + roles: [ROLE_NAME], + }); + + const { body } = await supertestWithoutAuth + .get('/api/upgrade_assistant/privileges') + .auth(USER_NAME, USER_PASSWORD) + .set('kbn-xsrf', 'kibana') + .send() + .expect(200); + + expect(body.hasAllPrivileges).to.be(false); + expect(body.missingPrivileges.index[0]).to.be(DEPRECATION_LOGS_INDEX); + } finally { + await security.role.delete(ROLE_NAME); + await security.user.delete(USER_NAME); + } + }); + }); + }); +} diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 7740f612bb117..e2c2e0b52dfdc 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -43,7 +43,7 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi serverArgs: [ ...xPackFunctionalTestsConfig.get('esTestCluster.serverArgs'), 'node.attr.name=apiIntegrationTestNode', - 'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2', + 'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2,/tmp/cloud-snapshots/', ], }, }; diff --git a/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts b/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts new file mode 100644 index 0000000000000..3024f8a5a7208 --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts @@ -0,0 +1,105 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const multiFieldsIndexDeprecation: estypes.IndicesCreateRequest = { + index: 'nested_multi_fields', + body: { + mappings: { + properties: { + text: { + type: 'text', + fields: { + english: { + type: 'text', + analyzer: 'english', + fields: { + english: { + type: 'text', + analyzer: 'english', + }, + }, + }, + }, + }, + }, + }, + }, +}; + +const translogSettingsIndexDeprecation: estypes.IndicesCreateRequest = { + index: 'deprecated_settings', + body: { + settings: { + 'translog.retention.size': '1b', + 'translog.retention.age': '5m', + 'index.soft_deletes.enabled': true, + }, + }, +}; + +export default function upgradeAssistantFunctionalTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const PageObjects = getPageObjects(['upgradeAssistant', 'common']); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const es = getService('es'); + const security = getService('security'); + const log = getService('log'); + + describe.skip('Deprecation pages', function () { + this.tags('skipFirefox'); + + before(async () => { + await security.testUser.setRoles(['global_upgrade_assistant_role']); + + try { + // Create two indices that will trigger deprecation warnings to test the ES deprecations page + await es.indices.create(multiFieldsIndexDeprecation); + await es.indices.create(translogSettingsIndexDeprecation); + } catch (e) { + log.debug('[Setup error] Error creating indices'); + throw e; + } + }); + + after(async () => { + try { + await es.indices.delete({ + index: [multiFieldsIndexDeprecation.index, translogSettingsIndexDeprecation.index], + }); + } catch (e) { + log.debug('[Cleanup error] Error deleting indices'); + throw e; + } + + await security.testUser.restoreDefaults(); + }); + + it('renders the Elasticsearch deprecations page', async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + await PageObjects.upgradeAssistant.clickEsDeprecationsPanel(); + + await retry.waitFor('Elasticsearch deprecations table to be visible', async () => { + return testSubjects.exists('esDeprecationsTable'); + }); + }); + + it('renders the Kibana deprecations page', async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + await PageObjects.upgradeAssistant.clickKibanaDeprecationsPanel(); + + await retry.waitFor('Kibana deprecations table to be visible', async () => { + return testSubjects.exists('kibanaDeprecations'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts index 7aa8bfe4eff69..dca3391ae5463 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['common', 'settings', 'security']); const appsMenu = getService('appsMenu'); @@ -17,14 +16,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe.skip('security', function () { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); await PageObjects.common.navigateToApp('home'); }); - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); - }); - describe('global all privileges (aka kibana_admin)', () => { before(async () => { await security.testUser.setRoles(['kibana_admin'], true); diff --git a/x-pack/test/functional/apps/upgrade_assistant/index.ts b/x-pack/test/functional/apps/upgrade_assistant/index.ts index c25e0af414397..d99d1cd033327 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/index.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/index.ts @@ -8,10 +8,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function upgradeCheckup({ loadTestFile }: FtrProviderContext) { - describe('Upgrade checkup ', function upgradeAssistantTestSuite() { + describe('Upgrade Assistant', function upgradeAssistantTestSuite() { this.tags('ciGroup4'); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./deprecation_pages')); + loadTestFile(require.resolve('./overview_page')); }); } diff --git a/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts b/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts new file mode 100644 index 0000000000000..0b8d15695689a --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/overview_page.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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function upgradeAssistantOverviewPageFunctionalTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const PageObjects = getPageObjects(['upgradeAssistant', 'common']); + const retry = getService('retry'); + const security = getService('security'); + const testSubjects = getService('testSubjects'); + const es = getService('es'); + + describe.skip('Overview Page', function () { + this.tags('skipFirefox'); + + before(async () => { + await security.testUser.setRoles(['superuser']); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + beforeEach(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('shows coming soon prompt', async () => { + await retry.waitFor('Upgrade Assistant overview page to be visible', async () => { + return testSubjects.exists('comingSoonPrompt'); + }); + }); + + it('Should render all steps', async () => { + testSubjects.exists('backupStep-incomplete'); + testSubjects.exists('fixIssuesStep-incomplete'); + testSubjects.exists('fixLogsStep-incomplete'); + testSubjects.exists('upgradeStep'); + }); + + describe('fixLogsStep', () => { + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + // Access to system indices will be deprecated and should generate a deprecation log + await es.indices.get({ index: '.kibana' }); + // Only click deprecation logging toggle if its not already enabled + if (!(await testSubjects.isDisplayed('externalLinksTitle'))) { + await PageObjects.upgradeAssistant.clickDeprecationLoggingToggle(); + } + + await retry.waitFor('UA external links title to be present', async () => { + return testSubjects.isDisplayed('externalLinksTitle'); + }); + }); + + beforeEach(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('Shows warnings callout if there are deprecations', async () => { + testSubjects.exists('hasWarningsCallout'); + }); + + it('Shows no warnings callout if there are no deprecations', async () => { + await PageObjects.upgradeAssistant.clickResetLastCheckpointButton(); + testSubjects.exists('noWarningsCallout'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts deleted file mode 100644 index 93475c228ed2f..0000000000000 --- a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function upgradeAssistantFunctionalTests({ - getService, - getPageObjects, -}: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['upgradeAssistant', 'common']); - const security = getService('security'); - const log = getService('log'); - const retry = getService('retry'); - - // Updated for the hiding of the UA UI. - describe.skip('Upgrade Checkup', function () { - this.tags('skipFirefox'); - - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); - await security.testUser.setRoles(['global_upgrade_assistant_role']); - }); - - after(async () => { - await PageObjects.upgradeAssistant.waitForTelemetryHidden(); - await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); - await security.testUser.restoreDefaults(); - }); - - it.skip('allows user to navigate to upgrade checkup', async () => { - await PageObjects.upgradeAssistant.navigateToPage(); - }); - - it.skip('allows user to toggle deprecation logging', async () => { - log.debug('expect initial state to be ON'); - expect(await PageObjects.upgradeAssistant.deprecationLoggingEnabledLabel()).to.be('On'); - expect(await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled()).to.be(true); - - await retry.try(async () => { - log.debug('Now toggle to off'); - await PageObjects.upgradeAssistant.toggleDeprecationLogging(); - - log.debug('expect state to be OFF after toggle'); - expect(await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled()).to.be(false); - expect(await PageObjects.upgradeAssistant.deprecationLoggingEnabledLabel()).to.be('Off'); - }); - - log.debug('Now toggle back on.'); - await retry.try(async () => { - await PageObjects.upgradeAssistant.toggleDeprecationLogging(); - log.debug('expect state to be ON after toggle'); - expect(await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled()).to.be(true); - expect(await PageObjects.upgradeAssistant.deprecationLoggingEnabledLabel()).to.be('On'); - }); - }); - - it.skip('allows user to open cluster tab', async () => { - await PageObjects.upgradeAssistant.navigateToPage(); - await PageObjects.upgradeAssistant.clickTab('cluster'); - expect(await PageObjects.upgradeAssistant.issueSummaryText()).to.be( - 'You have no cluster issues.' - ); - }); - - it.skip('allows user to open indices tab', async () => { - await PageObjects.upgradeAssistant.navigateToPage(); - await PageObjects.upgradeAssistant.clickTab('indices'); - expect(await PageObjects.upgradeAssistant.issueSummaryText()).to.be( - 'You have no index issues.' - ); - }); - }); -} diff --git a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts index 211bcbbd59219..54d7f3d452123 100644 --- a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts +++ b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts @@ -11,7 +11,6 @@ export class UpgradeAssistantPageObject extends FtrService { private readonly retry = this.ctx.getService('retry'); private readonly log = this.ctx.getService('log'); private readonly browser = this.ctx.getService('browser'); - private readonly find = this.ctx.getService('find'); private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly common = this.ctx.getPageObject('common'); @@ -30,47 +29,57 @@ export class UpgradeAssistantPageObject extends FtrService { }); } - async toggleDeprecationLogging() { - this.log.debug('toggleDeprecationLogging()'); - await this.testSubjects.click('upgradeAssistantDeprecationToggle'); + async clickEsDeprecationsPanel() { + return await this.retry.try(async () => { + await this.testSubjects.click('esStatsPanel'); + }); } - async isDeprecationLoggingEnabled() { - const isDeprecationEnabled = await this.testSubjects.getAttribute( - 'upgradeAssistantDeprecationToggle', - 'aria-checked' - ); - this.log.debug(`Deprecation enabled == ${isDeprecationEnabled}`); - return isDeprecationEnabled === 'true'; + async clickDeprecationLoggingToggle() { + return await this.retry.try(async () => { + await this.testSubjects.click('deprecationLoggingToggle'); + }); } - async deprecationLoggingEnabledLabel() { - const loggingEnabledLabel = await this.find.byCssSelector( - '[data-test-subj="upgradeAssistantDeprecationToggle"] ~ span' - ); - return await loggingEnabledLabel.getVisibleText(); + async clickResetLastCheckpointButton() { + return await this.retry.try(async () => { + await this.testSubjects.click('resetLastStoredDate'); + }); } - async clickTab(tabId: string) { + async clickKibanaDeprecationsPanel() { return await this.retry.try(async () => { - this.log.debug('clickTab()'); - await this.find.clickByCssSelector(`.euiTabs .euiTab#${tabId}`); + await this.testSubjects.click('kibanaStatsPanel'); }); } - async waitForTelemetryHidden() { - const self = this; - await this.retry.waitFor('Telemetry to disappear.', async () => { - return (await self.isTelemetryExists()) === false; + async clickKibanaDeprecation(selectedIssue: string) { + const table = await this.testSubjects.find('kibanaDeprecationsTable'); + const rows = await table.findAllByTestSubject('row'); + + const selectedRow = rows.find(async (row) => { + const issue = await (await row.findByTestSubject('issueCell')).getVisibleText(); + return issue === selectedIssue; }); - } - async issueSummaryText() { - this.log.debug('expectIssueSummary()'); - return await this.testSubjects.getVisibleText('upgradeAssistantIssueSummary'); + if (selectedRow) { + const issueLink = await selectedRow.findByTestSubject('deprecationDetailsLink'); + await issueLink.click(); + } else { + this.log.debug('Unable to find selected deprecation row'); + } } - async isTelemetryExists() { - return await this.testSubjects.exists('upgradeAssistantTelemetryRunning'); + async clickEsDeprecation(deprecationType: 'indexSettings' | 'default' | 'reindex' | 'ml') { + const table = await this.testSubjects.find('esDeprecationsTable'); + const deprecationIssueLink = await ( + await table.findByTestSubject(`${deprecationType}TableCell-message`) + ).findByCssSelector('button'); + + if (deprecationIssueLink) { + await deprecationIssueLink.click(); + } else { + this.log.debug('Unable to find selected deprecation'); + } } } From c48ef8a0ab7f1b17d0eadc90c089576c8256331e Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Tue, 9 Nov 2021 16:59:00 +0300 Subject: [PATCH 32/98] [Vega] Debounce duplicate error messages (#116408) * [Vega] Debounce duplicate error messages * Update filter logic to avoid errors Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vega/public/vega_view/vega_base_view.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js index 1c444e7528d44..8c725ba0a75a2 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js @@ -247,11 +247,16 @@ export class VegaBaseView { if (!this._$messages) { this._$messages = $(`
    `).appendTo(this._$parentEl); } - this._$messages.append( - $(`
  • `).append( - $(`
    `).text(text)
    -      )
    -    );
    +    const isMessageAlreadyDisplayed = this._$messages
    +      .find(`pre.vgaVis__messageCode`)
    +      .filter((index, element) => element.textContent === text).length;
    +    if (!isMessageAlreadyDisplayed) {
    +      this._$messages.append(
    +        $(`
  • `).append( + $(`
    `).text(text)
    +        )
    +      );
    +    }
       }
     
       resize() {
    
    From a815bd271a3b94ee98c1b10635fe414006cd11e3 Mon Sep 17 00:00:00 2001
    From: Dominique Clarke 
    Date: Tue, 9 Nov 2021 09:03:00 -0500
    Subject: [PATCH 33/98] [User Experience] add title to UX data view (#117859)
    
    * add title to UX data view
    
    * update useDataView
    ---
     .../LocalUIFilters/use_data_view.test.js      | 44 +++++++++++++++++++
     .../LocalUIFilters/use_data_view.ts           |  9 ++--
     2 files changed, 47 insertions(+), 6 deletions(-)
     create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js
    
    diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js
    new file mode 100644
    index 0000000000000..d9eef896782ca
    --- /dev/null
    +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.test.js
    @@ -0,0 +1,44 @@
    +/*
    + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
    + * or more contributor license agreements. Licensed under the Elastic License
    + * 2.0; you may not use this file except in compliance with the Elastic License
    + * 2.0.
    + */
    +import React from 'react';
    +import { renderHook } from '@testing-library/react-hooks';
    +import * as dynamicDataView from '../../../../hooks/use_dynamic_data_view';
    +import { useDataView } from './use_data_view';
    +import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
    +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
    +
    +describe('useDataView', () => {
    +  const create = jest.fn();
    +  const mockDataService = {
    +    data: {
    +      dataViews: {
    +        create,
    +      },
    +    },
    +  };
    +
    +  const title = 'apm-*';
    +  jest
    +    .spyOn(dynamicDataView, 'useDynamicDataViewFetcher')
    +    .mockReturnValue({ dataView: { title } });
    +
    +  it('returns result as expected', async () => {
    +    const { waitForNextUpdate } = renderHook(() => useDataView(), {
    +      wrapper: ({ children }) => (
    +        
    +          
    +            {children}
    +          
    +        
    +      ),
    +    });
    +
    +    await waitForNextUpdate();
    +
    +    expect(create).toBeCalledWith({ title });
    +  });
    +});
    diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts
    index ba99729293368..40d0017d8d096 100644
    --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts
    +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/use_data_view.ts
    @@ -6,10 +6,7 @@
      */
     
     import { useDynamicDataViewFetcher } from '../../../../hooks/use_dynamic_data_view';
    -import {
    -  DataView,
    -  DataViewSpec,
    -} from '../../../../../../../../src/plugins/data/common';
    +import { DataView } from '../../../../../../../../src/plugins/data/common';
     import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
     import { useFetcher } from '../../../../hooks/use_fetcher';
     import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public';
    @@ -26,8 +23,8 @@ export function useDataView() {
       const { data } = useFetcher>(async () => {
         if (dataView?.title) {
           return dataViews.create({
    -        pattern: dataView?.title,
    -      } as DataViewSpec);
    +        title: dataView?.title,
    +      });
         }
       }, [dataView?.title, dataViews]);
     
    
    From df0d9a6959e03dbad52631a2949c4618bd375723 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
     <55978943+cauemarcondes@users.noreply.github.com>
    Date: Tue, 9 Nov 2021 09:03:14 -0500
    Subject: [PATCH 34/98] [APM] Trace waterfall is visually broken (#117589)
    
    * fixing accordion
    
    * fixing trace waterfall
    
    * removing import
    
    * fixing test
    
    * addressing pr changes
    
    Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
    ---
     .../Waterfall/accordion_waterfall.tsx         | 139 +++++++++++-------
     1 file changed, 89 insertions(+), 50 deletions(-)
    
    diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx
    index 15883e7905142..5d089e53bd998 100644
    --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx
    +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx
    @@ -5,16 +5,22 @@
      * 2.0.
      */
     
    -import { EuiAccordion, EuiAccordionProps } from '@elastic/eui';
    -import { isEmpty } from 'lodash';
    +import {
    +  EuiAccordion,
    +  EuiAccordionProps,
    +  EuiFlexGroup,
    +  EuiFlexItem,
    +  EuiIcon,
    +  EuiText,
    +} from '@elastic/eui';
     import React, { Dispatch, SetStateAction, useState } from 'react';
     import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
     import { Margins } from '../../../../../shared/charts/Timeline';
    -import { WaterfallItem } from './waterfall_item';
     import {
       IWaterfall,
       IWaterfallSpanOrTransaction,
     } from './waterfall_helpers/waterfall_helpers';
    +import { WaterfallItem } from './waterfall_item';
     
     interface AccordionWaterfallProps {
       isOpen: boolean;
    @@ -28,6 +34,8 @@ interface AccordionWaterfallProps {
       onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void;
     }
     
    +const ACCORDION_HEIGHT = '48px';
    +
     const StyledAccordion = euiStyled(EuiAccordion).withConfig({
       shouldForwardProp: (prop) =>
         !['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop),
    @@ -38,54 +46,33 @@ const StyledAccordion = euiStyled(EuiAccordion).withConfig({
         hasError: boolean;
       }
     >`
    -  .euiAccordion {
    +  .waterfall_accordion {
         border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
       }
    -  .euiIEFlexWrapFix {
    -    width: 100%;
    -    height: 48px;
    -  }
    +
       .euiAccordion__childWrapper {
         transition: none;
       }
     
    -  .euiAccordion__padding--l {
    -    padding-top: 0;
    -    padding-bottom: 0;
    -  }
    -
    -  .euiAccordion__iconWrapper {
    -    display: flex;
    -    position: relative;
    -    &:after {
    -      content: ${(props) => `'${props.childrenCount}'`};
    -      position: absolute;
    -      left: 20px;
    -      top: -1px;
    -      z-index: 1;
    -      font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
    -    }
    -  }
    -
       ${(props) => {
         const borderLeft = props.hasError
           ? `2px solid ${props.theme.eui.euiColorDanger};`
           : `1px solid ${props.theme.eui.euiColorLightShade};`;
         return `.button_${props.id} {
    +      width: 100%;
    +      height: ${ACCORDION_HEIGHT};
           margin-left: ${props.marginLeftLevel}px;
           border-left: ${borderLeft}
           &:hover {
             background-color: ${props.theme.eui.euiColorLightestShade};
           }
         }`;
    -    //
       }}
    -`;
     
    -const WaterfallItemContainer = euiStyled.div`
    -  position: absolute;
    -  width: 100%;
    -  left: 0;
    +  .accordion__buttonContent {
    +    width: 100%;
    +    height: 100%;
    +  }
     `;
     
     export function AccordionWaterfall(props: AccordionWaterfallProps) {
    @@ -111,36 +98,51 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
       // To indent the items creating the parent/child tree
       const marginLeftLevel = 8 * level;
     
    +  function toggleAccordion() {
    +    setIsOpen((isCurrentOpen) => !isCurrentOpen);
    +  }
    +
       return (
         
    -           {
    -              onClickWaterfallItem(item);
    -            }}
    -          />
    -        
    +        
    +          
    +            
    +          
    +          
    +             {
    +                onClickWaterfallItem(item);
    +              }}
    +            />
    +          
    +        
           }
    -      arrowDisplay={isEmpty(children) ? 'none' : 'left'}
    +      arrowDisplay="none"
           initialIsOpen={true}
           forceState={isOpen ? 'open' : 'closed'}
    -      onToggle={() => {
    -        setIsOpen((isCurrentOpen) => !isCurrentOpen);
    -      }}
    +      onToggle={toggleAccordion}
         >
           {children.map((child) => (
             
       );
     }
    +
    +function ToggleAccordionButton({
    +  show,
    +  isOpen,
    +  childrenAmount,
    +  onClick,
    +}: {
    +  show: boolean;
    +  isOpen: boolean;
    +  childrenAmount: number;
    +  onClick: () => void;
    +}) {
    +  if (!show) {
    +    return null;
    +  }
    +
    +  return (
    +    
    + + + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
    { + e.stopPropagation(); + onClick(); + }} + > + +
    +
    + + {childrenAmount} + +
    +
    + ); +} From 502db21a3d4a1caa2a65f019d1409ca814b7d17d Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 9 Nov 2021 09:16:29 -0500 Subject: [PATCH 35/98] [Uptime] Ping redirects - add retry logic (#117363) * add retry logic and focus test * unfocus tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/functional/apps/uptime/ping_redirects.ts | 3 +-- x-pack/test/functional/services/uptime/navigation.ts | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/ping_redirects.ts b/x-pack/test/functional/apps/uptime/ping_redirects.ts index 748163cb5ec78..03185ac9f1466 100644 --- a/x-pack/test/functional/apps/uptime/ping_redirects.ts +++ b/x-pack/test/functional/apps/uptime/ping_redirects.ts @@ -19,8 +19,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const monitor = () => uptime.monitor; - // FLAKY: https://github.com/elastic/kibana/issues/84992 - describe.skip('Ping redirects', () => { + describe('Ping redirects', () => { const start = '~ 15 minutes ago'; const end = 'now'; diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index 51806d1006ab4..43d62ef74bf31 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -61,7 +61,9 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv goToMonitor: async (monitorId: string) => { // only go to monitor page if not already there if (!(await testSubjects.exists('uptimeMonitorPage', { timeout: 0 }))) { - await testSubjects.click(`monitor-page-link-${monitorId}`); + return retry.try(async () => { + await testSubjects.click(`monitor-page-link-${monitorId}`); + }); await testSubjects.existOrFail('uptimeMonitorPage', { timeout: 30000, }); From df71a4e8724c7863bce73abbccfdd1ae9b3e80c7 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 9 Nov 2021 08:41:19 -0600 Subject: [PATCH 36/98] [DOCS] Removes coming tag from 8.0.0-beta1 release notes (#117045) --- docs/CHANGELOG.asciidoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 8111172893795..31cdcbca9d1f9 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -18,8 +18,6 @@ Review important information about the {kib} 8.0.0 releases. [[release-notes-8.0.0-beta1]] == {kib} 8.0.0-beta1 -coming::[8.0.0-beta1] - Review the {kib} 8.0.0-beta1 changes, then use the <> to complete the upgrade. [float] From c7c759f2f85121d0dd4397858061e0fb42500c25 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 9 Nov 2021 08:55:09 -0600 Subject: [PATCH 37/98] [build] Add download cloud dependencies task (#117227) * [build] Add download cloud dependencies task This adds a task to download filebeat and metricbeat for use in building our cloud image. Previously, these were run using local artifacts added by the release manager. As we transition towards building our own releasable artifacts, we need to be able to fetch these dependencies at build time. This includes argument changes to the build command: - Docker cloud images are built by default, to skip add `--skip-docker-cloud`. `--docker-cloud` has been removed to be consistent with other arguments. - Artifacts are downloaded by default, to use local artifacts add `--skip-cloud-dependencies-download` * fix checks * build cloud image with ci:deploy-cloud Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/scripts/build_kibana.sh | 13 ++++++ src/dev/build/args.test.ts | 15 +++++-- src/dev/build/args.ts | 6 ++- src/dev/build/build_distributables.ts | 7 +++- src/dev/build/lib/download.ts | 19 +++++---- .../lib/integration_tests/download.test.ts | 15 ++++--- .../tasks/download_cloud_dependencies.ts | 42 +++++++++++++++++++ src/dev/build/tasks/index.ts | 1 + .../nodejs/download_node_builds_task.test.ts | 15 ++++--- .../tasks/nodejs/download_node_builds_task.ts | 3 +- .../build/tasks/patch_native_modules_task.ts | 3 +- 11 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 src/dev/build/tasks/download_cloud_dependencies.ts diff --git a/.buildkite/scripts/build_kibana.sh b/.buildkite/scripts/build_kibana.sh index e26d7790215f3..84d66a30ea213 100755 --- a/.buildkite/scripts/build_kibana.sh +++ b/.buildkite/scripts/build_kibana.sh @@ -11,6 +11,19 @@ else node scripts/build fi +if [[ "${GITHUB_PR_LABELS:-}" == *"ci:deploy-cloud"* ]]; then + echo "--- Build Kibana Cloud Distribution" + node scripts/build \ + --skip-initialize \ + --skip-generic-folders \ + --skip-platform-folders \ + --skip-archives \ + --docker-images \ + --skip-docker-ubi \ + --skip-docker-centos \ + --skip-docker-contexts +fi + echo "--- Archive Kibana Distribution" linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" diff --git a/src/dev/build/args.test.ts b/src/dev/build/args.test.ts index 64d89a650e62e..9ac4c5ec3b236 100644 --- a/src/dev/build/args.test.ts +++ b/src/dev/build/args.test.ts @@ -36,6 +36,7 @@ it('build default and oss dist for current platform, without packages, by defaul "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -56,13 +57,14 @@ it('builds packages if --all-platforms is passed', () => { "createArchives": true, "createDebPackage": true, "createDockerCentOS": true, - "createDockerCloud": false, + "createDockerCloud": true, "createDockerContexts": true, "createDockerUBI": true, "createExamplePlugins": false, "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -90,6 +92,7 @@ it('limits packages if --rpm passed with --all-platforms', () => { "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -117,6 +120,7 @@ it('limits packages if --deb passed with --all-platforms', () => { "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -138,13 +142,14 @@ it('limits packages if --docker passed with --all-platforms', () => { "createArchives": true, "createDebPackage": false, "createDockerCentOS": true, - "createDockerCloud": false, + "createDockerCloud": true, "createDockerContexts": true, "createDockerUBI": true, "createExamplePlugins": false, "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -173,13 +178,14 @@ it('limits packages if --docker passed with --skip-docker-ubi and --all-platform "createArchives": true, "createDebPackage": false, "createDockerCentOS": true, - "createDockerCloud": false, + "createDockerCloud": true, "createDockerContexts": true, "createDockerUBI": false, "createExamplePlugins": false, "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": false, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, @@ -201,13 +207,14 @@ it('limits packages if --all-platforms passed with --skip-docker-centos', () => "createArchives": true, "createDebPackage": true, "createDockerCentOS": false, - "createDockerCloud": false, + "createDockerCloud": true, "createDockerContexts": true, "createDockerUBI": true, "createExamplePlugins": false, "createGenericFolders": true, "createPlatformFolders": true, "createRpmPackage": true, + "downloadCloudDependencies": true, "downloadFreshNode": true, "initialize": true, "isRelease": false, diff --git a/src/dev/build/args.ts b/src/dev/build/args.ts index 1124d90be89c6..e7fca2a2a3d7b 100644 --- a/src/dev/build/args.ts +++ b/src/dev/build/args.ts @@ -26,9 +26,10 @@ export function readCliArgs(argv: string[]) { 'skip-docker-contexts', 'skip-docker-ubi', 'skip-docker-centos', - 'docker-cloud', + 'skip-docker-cloud', 'release', 'skip-node-download', + 'skip-cloud-dependencies-download', 'verbose', 'debug', 'all-platforms', @@ -96,6 +97,7 @@ export function readCliArgs(argv: string[]) { versionQualifier: flags['version-qualifier'], initialize: !Boolean(flags['skip-initialize']), downloadFreshNode: !Boolean(flags['skip-node-download']), + downloadCloudDependencies: !Boolean(flags['skip-cloud-dependencies-download']), createGenericFolders: !Boolean(flags['skip-generic-folders']), createPlatformFolders: !Boolean(flags['skip-platform-folders']), createArchives: !Boolean(flags['skip-archives']), @@ -104,7 +106,7 @@ export function readCliArgs(argv: string[]) { createDebPackage: isOsPackageDesired('deb'), createDockerCentOS: isOsPackageDesired('docker-images') && !Boolean(flags['skip-docker-centos']), - createDockerCloud: isOsPackageDesired('docker-images') && Boolean(flags['docker-cloud']), + createDockerCloud: isOsPackageDesired('docker-images') && !Boolean(flags['skip-docker-cloud']), createDockerUBI: isOsPackageDesired('docker-images') && !Boolean(flags['skip-docker-ubi']), createDockerContexts: !Boolean(flags['skip-docker-contexts']), targetAllPlatforms: Boolean(flags['all-platforms']), diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 39a62c1fd35dc..8912b05a16943 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -14,6 +14,7 @@ import * as Tasks from './tasks'; export interface BuildOptions { isRelease: boolean; downloadFreshNode: boolean; + downloadCloudDependencies: boolean; initialize: boolean; createGenericFolders: boolean; createPlatformFolders: boolean; @@ -129,7 +130,11 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions } if (options.createDockerCloud) { - // control w/ --docker-images and --docker-cloud + // control w/ --docker-images and --skip-docker-cloud + if (options.downloadCloudDependencies) { + // control w/ --skip-cloud-dependencies-download + await run(Tasks.DownloadCloudDependencies); + } await run(Tasks.CreateDockerCloud); } diff --git a/src/dev/build/lib/download.ts b/src/dev/build/lib/download.ts index ce2bdbd33e8c1..9293854bfb2bd 100644 --- a/src/dev/build/lib/download.ts +++ b/src/dev/build/lib/download.ts @@ -34,14 +34,15 @@ interface DownloadOptions { log: ToolingLog; url: string; destination: string; - sha256: string; + shaChecksum: string; + shaAlgorithm: string; retries?: number; } export async function download(options: DownloadOptions): Promise { - const { log, url, destination, sha256, retries = 0 } = options; + const { log, url, destination, shaChecksum, shaAlgorithm, retries = 0 } = options; - if (!sha256) { - throw new Error(`sha256 checksum of ${url} not provided, refusing to download.`); + if (!shaChecksum) { + throw new Error(`${shaAlgorithm} checksum of ${url} not provided, refusing to download.`); } // mkdirp and open file outside of try/catch, we don't retry for those errors @@ -50,7 +51,7 @@ export async function download(options: DownloadOptions): Promise { let error; try { - log.debug(`Attempting download of ${url}`, chalk.dim(sha256)); + log.debug(`Attempting download of ${url}`, chalk.dim(shaAlgorithm)); const response = await Axios.request({ url, @@ -62,7 +63,7 @@ export async function download(options: DownloadOptions): Promise { throw new Error(`Unexpected status code ${response.status} when downloading ${url}`); } - const hash = createHash('sha256'); + const hash = createHash(shaAlgorithm); await new Promise((resolve, reject) => { response.data.on('data', (chunk: Buffer) => { hash.update(chunk); @@ -73,10 +74,10 @@ export async function download(options: DownloadOptions): Promise { response.data.on('end', resolve); }); - const downloadedSha256 = hash.digest('hex'); - if (downloadedSha256 !== sha256) { + const downloadedSha = hash.digest('hex'); + if (downloadedSha !== shaChecksum) { throw new Error( - `Downloaded checksum ${downloadedSha256} does not match the expected sha256 checksum.` + `Downloaded checksum ${downloadedSha} does not match the expected ${shaAlgorithm} checksum.` ); } } catch (_error) { diff --git a/src/dev/build/lib/integration_tests/download.test.ts b/src/dev/build/lib/integration_tests/download.test.ts index 9003e678e98a8..173682ef05d71 100644 --- a/src/dev/build/lib/integration_tests/download.test.ts +++ b/src/dev/build/lib/integration_tests/download.test.ts @@ -93,7 +93,8 @@ it('downloads from URL and checks that content matches sha256', async () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: FOO_SHA256, + shaChecksum: FOO_SHA256, + shaAlgorithm: 'sha256', }); expect(readFileSync(TMP_DESTINATION, 'utf8')).toBe('foo'); }); @@ -106,7 +107,8 @@ it('rejects and deletes destination if sha256 does not match', async () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: 'bar', + shaChecksum: 'bar', + shaAlgorithm: 'sha256', }); throw new Error('Expected download() to reject'); } catch (error) { @@ -141,7 +143,8 @@ describe('reties download retries: number of times', () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: FOO_SHA256, + shaChecksum: FOO_SHA256, + shaAlgorithm: 'sha256', retries: 2, }); @@ -167,7 +170,8 @@ describe('reties download retries: number of times', () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: FOO_SHA256, + shaChecksum: FOO_SHA256, + shaAlgorithm: 'sha256', retries: 2, }); }); @@ -185,7 +189,8 @@ describe('reties download retries: number of times', () => { log, url: serverUrl, destination: TMP_DESTINATION, - sha256: FOO_SHA256, + shaChecksum: FOO_SHA256, + shaAlgorithm: 'sha256', retries: 5, }); throw new Error('Expected download() to reject'); diff --git a/src/dev/build/tasks/download_cloud_dependencies.ts b/src/dev/build/tasks/download_cloud_dependencies.ts new file mode 100644 index 0000000000000..5b5ba2a9ff625 --- /dev/null +++ b/src/dev/build/tasks/download_cloud_dependencies.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 axios from 'axios'; +import Path from 'path'; +import del from 'del'; +import { Task, download } from '../lib'; + +export const DownloadCloudDependencies: Task = { + description: 'Downloading cloud dependencies', + + async run(config, log, build) { + const downloadBeat = async (beat: string) => { + const subdomain = config.isRelease ? 'artifacts' : 'snapshots'; + const version = config.getBuildVersion(); + const architecture = process.arch === 'arm64' ? 'arm64' : 'x86_64'; + const url = `https://${subdomain}-no-kpi.elastic.co/downloads/beats/${beat}/${beat}-${version}-linux-${architecture}.tar.gz`; + const checksumRes = await axios.get(url + '.sha512'); + if (checksumRes.status !== 200) { + throw new Error(`Unexpected status code ${checksumRes.status} when downloading ${url}`); + } + const destination = config.resolveFromRepo('.beats', Path.basename(url)); + return download({ + log, + url, + destination, + shaChecksum: checksumRes.data.split(' ')[0], + shaAlgorithm: 'sha512', + retries: 3, + }); + }; + + await del([config.resolveFromRepo('.beats')]); + await downloadBeat('metricbeat'); + await downloadBeat('filebeat'); + }, +}; diff --git a/src/dev/build/tasks/index.ts b/src/dev/build/tasks/index.ts index 35d35023399db..5043be288928e 100644 --- a/src/dev/build/tasks/index.ts +++ b/src/dev/build/tasks/index.ts @@ -16,6 +16,7 @@ export * from './create_archives_sources_task'; export * from './create_archives_task'; export * from './create_empty_dirs_and_files_task'; export * from './create_readme_task'; +export * from './download_cloud_dependencies'; export * from './generate_packages_optimized_assets'; export * from './install_dependencies_task'; export * from './license_file_task'; diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts index 31374d2050971..ec82caac273cf 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts @@ -76,7 +76,8 @@ it('downloads node builds for each platform', async () => { "destination": "linux:downloadPath", "log": , "retries": 3, - "sha256": "linux:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "linux:sha256", "url": "linux:url", }, ], @@ -85,7 +86,8 @@ it('downloads node builds for each platform', async () => { "destination": "linux:downloadPath", "log": , "retries": 3, - "sha256": "linux:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "linux:sha256", "url": "linux:url", }, ], @@ -94,7 +96,8 @@ it('downloads node builds for each platform', async () => { "destination": "darwin:downloadPath", "log": , "retries": 3, - "sha256": "darwin:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "darwin:sha256", "url": "darwin:url", }, ], @@ -103,7 +106,8 @@ it('downloads node builds for each platform', async () => { "destination": "darwin:downloadPath", "log": , "retries": 3, - "sha256": "darwin:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "darwin:sha256", "url": "darwin:url", }, ], @@ -112,7 +116,8 @@ it('downloads node builds for each platform', async () => { "destination": "win32:downloadPath", "log": , "retries": 3, - "sha256": "win32:sha256", + "shaAlgorithm": "sha256", + "shaChecksum": "win32:sha256", "url": "win32:url", }, ], diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.ts index c0c7a399f84cf..f19195092d964 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.ts @@ -21,7 +21,8 @@ export const DownloadNodeBuilds: GlobalTask = { await download({ log, url, - sha256: shasums[downloadName], + shaChecksum: shasums[downloadName], + shaAlgorithm: 'sha256', destination: downloadPath, retries: 3, }); diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index 37cb729053785..fe9743533b901 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -104,7 +104,8 @@ async function patchModule( log, url: archive.url, destination: downloadPath, - sha256: archive.sha256, + shaChecksum: archive.sha256, + shaAlgorithm: 'sha256', retries: 3, }); switch (pkg.extractMethod) { From 55b494410996176d0d0d49d070e36da2b65ecc74 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 9 Nov 2021 10:47:34 -0500 Subject: [PATCH 38/98] skip flaky suite (#100570) --- x-pack/test/functional/apps/spaces/enter_space.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index e1dc70b81e146..25fdcfda395ce 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -14,7 +14,8 @@ export default function enterSpaceFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'spaceSelector']); - describe('Enter Space', function () { + // Failing: See https://github.com/elastic/kibana/issues/100570 + describe.skip('Enter Space', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/spaces/enter_space'); From 903df6d80e64906cd111d446d186dc87281f63df Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 9 Nov 2021 07:50:44 -0800 Subject: [PATCH 39/98] [Alerting][Telemetry] Reverted type changes for throttle_time and schedule_time with adding a proper new number fields (#117805) * [Alerting][Telemetry] Rverted type changes for throttle_time and schedule_time with adding a new number fields * fixed types * fixed types * fixed due to comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/usage/alerts_telemetry.test.ts | 10 +++++++ .../alerting/server/usage/alerts_telemetry.ts | 16 ++++++++++-- .../server/usage/alerts_usage_collector.ts | 24 +++++++++++++++-- x-pack/plugins/alerting/server/usage/types.ts | 12 ++++++++- .../schema/xpack_plugins.json | 26 +++++++++++++++++++ 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts index af08c8c75c144..848c5e9b72168 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts @@ -106,11 +106,21 @@ Object { "count_rules_namespaces": 0, "count_total": 4, "schedule_time": Object { + "avg": "4.5s", + "max": "10s", + "min": "1s", + }, + "schedule_time_number_s": Object { "avg": 4.5, "max": 10, "min": 1, }, "throttle_time": Object { + "avg": "30s", + "max": "60s", + "min": "0s", + }, + "throttle_time_number_s": Object { "avg": 30, "max": 60, "min": 0, diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index 180ee4300f18c..075404e82e1a9 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -13,7 +13,7 @@ const alertTypeMetric = { init_script: 'state.ruleTypes = [:]; state.namespaces = [:]', map_script: ` String alertType = doc['alert.alertTypeId'].value; - String namespace = doc['namespaces'] !== null ? doc['namespaces'].value : 'default'; + String namespace = doc['namespaces'] !== null && doc['namespaces'].size() > 0 ? doc['namespaces'].value : 'default'; state.ruleTypes.put(alertType, state.ruleTypes.containsKey(alertType) ? state.ruleTypes.get(alertType) + 1 : 1); if (state.namespaces.containsKey(namespace) === false) { state.namespaces.put(namespace, 1); @@ -107,6 +107,8 @@ export async function getTotalCountAggregations( | 'count_by_type' | 'throttle_time' | 'schedule_time' + | 'throttle_time_number_s' + | 'schedule_time_number_s' | 'connectors_per_alert' | 'count_rules_namespaces' > @@ -253,11 +255,21 @@ export async function getTotalCountAggregations( {} ), throttle_time: { + min: `${aggregations.min_throttle_time.value}s`, + avg: `${aggregations.avg_throttle_time.value}s`, + max: `${aggregations.max_throttle_time.value}s`, + }, + schedule_time: { + min: `${aggregations.min_interval_time.value}s`, + avg: `${aggregations.avg_interval_time.value}s`, + max: `${aggregations.max_interval_time.value}s`, + }, + throttle_time_number_s: { min: aggregations.min_throttle_time.value, avg: aggregations.avg_throttle_time.value, max: aggregations.max_throttle_time.value, }, - schedule_time: { + schedule_time_number_s: { min: aggregations.min_interval_time.value, avg: aggregations.avg_interval_time.value, max: aggregations.max_interval_time.value, diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts index e5b25ea75fc1c..327073f26bacf 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts @@ -95,11 +95,21 @@ export function createAlertsUsageCollector( count_active_total: 0, count_disabled_total: 0, throttle_time: { + min: '0s', + avg: '0s', + max: '0s', + }, + schedule_time: { + min: '0s', + avg: '0s', + max: '0s', + }, + throttle_time_number_s: { min: 0, avg: 0, max: 0, }, - schedule_time: { + schedule_time_number_s: { min: 0, avg: 0, max: 0, @@ -127,11 +137,21 @@ export function createAlertsUsageCollector( count_active_total: { type: 'long' }, count_disabled_total: { type: 'long' }, throttle_time: { + min: { type: 'keyword' }, + avg: { type: 'keyword' }, + max: { type: 'keyword' }, + }, + schedule_time: { + min: { type: 'keyword' }, + avg: { type: 'keyword' }, + max: { type: 'keyword' }, + }, + throttle_time_number_s: { min: { type: 'long' }, avg: { type: 'float' }, max: { type: 'long' }, }, - schedule_time: { + schedule_time_number_s: { min: { type: 'long' }, avg: { type: 'float' }, max: { type: 'long' }, diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerting/server/usage/types.ts index 50d9b80c44b70..546663e3ea403 100644 --- a/x-pack/plugins/alerting/server/usage/types.ts +++ b/x-pack/plugins/alerting/server/usage/types.ts @@ -20,11 +20,21 @@ export interface AlertsUsage { avg_execution_time_per_day: number; avg_execution_time_by_type_per_day: Record; throttle_time: { + min: string; + avg: string; + max: string; + }; + schedule_time: { + min: string; + avg: string; + max: string; + }; + throttle_time_number_s: { min: number; avg: number; max: number; }; - schedule_time: { + schedule_time_number_s: { min: number; avg: number; max: number; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index e9008a1196700..13caed52ded84 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -271,6 +271,19 @@ "type": "long" }, "throttle_time": { + "properties": { + "min": { + "type": "keyword" + }, + "avg": { + "type": "keyword" + }, + "max": { + "type": "keyword" + } + } + }, + "throttle_time_number_s": { "properties": { "min": { "type": "long" @@ -284,6 +297,19 @@ } }, "schedule_time": { + "properties": { + "min": { + "type": "keyword" + }, + "avg": { + "type": "keyword" + }, + "max": { + "type": "keyword" + } + } + }, + "schedule_time_number_s": { "properties": { "min": { "type": "long" From 4bedc1cd93bab201197fc70d66726cd4296e96bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 9 Nov 2021 15:51:52 +0000 Subject: [PATCH 40/98] [Runtime field editor] Improve error handling (#109233) --- packages/kbn-monaco/BUILD.bazel | 2 + packages/kbn-monaco/__jest__/jest.mocks.ts | 68 +++ packages/kbn-monaco/__jest__/types.ts | 19 + .../src/painless/diagnostics_adapter.test.ts | 147 +++++++ .../src/painless/diagnostics_adapter.ts | 53 ++- packages/kbn-monaco/src/painless/index.ts | 3 +- packages/kbn-monaco/src/painless/language.ts | 11 +- packages/kbn-monaco/src/types.ts | 21 + packages/kbn-monaco/tsconfig.json | 1 + .../docs/core/use_async_validation_data.mdx | 36 -- .../forms/docs/core/use_behavior_subject.mdx | 26 ++ .../static/forms/docs/core/use_field.mdx | 12 +- .../static/forms/docs/core/use_form_data.mdx | 25 ++ .../static/forms/docs/examples/validation.mdx | 21 +- .../components/use_field.test.tsx | 75 +--- .../hook_form_lib/components/use_field.tsx | 9 +- .../static/forms/hook_form_lib/hooks/index.ts | 2 +- .../hooks/use_async_validation_data.ts | 36 -- .../forms/hook_form_lib/hooks/use_field.ts | 45 +- .../hook_form_lib/hooks/use_form.test.tsx | 59 +++ .../forms/hook_form_lib/hooks/use_form.ts | 64 ++- .../hooks/use_form_data.test.tsx | 88 +++- .../hook_form_lib/hooks/use_form_data.ts | 42 +- .../forms/hook_form_lib/hooks/utils/index.ts | 9 + .../hooks/utils/use_behavior_subject.ts | 31 ++ .../static/forms/hook_form_lib/index.ts | 2 +- .../static/forms/hook_form_lib/types.ts | 27 +- .../field_editor.helpers.ts | 6 +- .../client_integration/field_editor.test.tsx | 112 +---- .../field_editor_flyout_content.helpers.ts | 3 +- .../field_editor_flyout_content.test.ts | 82 +++- .../field_editor_flyout_preview.helpers.ts | 8 +- .../field_editor_flyout_preview.test.ts | 397 ++++++++--------- .../helpers/common_actions.ts | 70 +-- .../client_integration/helpers/index.ts | 10 +- .../client_integration/helpers/jest.mocks.tsx | 47 +- .../client_integration/helpers/mocks.ts | 42 ++ .../helpers/setup_environment.tsx | 22 +- .../components/field_editor/field_editor.tsx | 36 +- .../field_editor/form_fields/index.ts | 1 - .../field_editor/form_fields/script_field.tsx | 261 ++++++----- .../components/field_editor/form_schema.ts | 79 +++- .../field_editor_flyout_content.tsx | 82 +--- .../field_editor_flyout_content_container.tsx | 8 +- .../preview/documents_nav_preview.tsx | 18 +- .../preview/field_list/field_list.scss | 6 +- .../preview/field_list/field_list.tsx | 7 +- .../preview/field_list/field_list_item.tsx | 90 +++- .../components/preview/field_preview.tsx | 121 ++++-- .../preview/field_preview_context.tsx | 404 ++++++++++-------- .../preview/field_preview_error.tsx | 18 +- .../preview/field_preview_header.tsx | 30 +- .../public/components/preview/index.ts | 2 + .../preview/is_updating_indicator.tsx | 27 ++ .../public/components/preview/types.ts | 139 ++++++ .../public/lib/api.ts | 4 +- .../public/lib/index.ts | 6 +- .../lib/runtime_field_validation.test.ts | 165 ------- .../public/lib/runtime_field_validation.ts | 111 +---- .../public/lib/serialization.ts | 21 +- .../public/shared_imports.ts | 3 + .../public/types.ts | 30 +- .../server/routes/field_preview.ts | 7 + .../field_preview.ts | 22 + .../management/_index_pattern_popularity.js | 4 +- test/functional/services/field_editor.ts | 8 +- .../queries/ecs_mapping_editor_field.tsx | 10 +- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 69 files changed, 2056 insertions(+), 1407 deletions(-) create mode 100644 packages/kbn-monaco/__jest__/jest.mocks.ts create mode 100644 packages/kbn-monaco/__jest__/types.ts create mode 100644 packages/kbn-monaco/src/painless/diagnostics_adapter.test.ts delete mode 100644 src/plugins/es_ui_shared/static/forms/docs/core/use_async_validation_data.mdx create mode 100644 src/plugins/es_ui_shared/static/forms/docs/core/use_behavior_subject.mdx delete mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_async_validation_data.ts create mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/index.ts create mode 100644 src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/use_behavior_subject.ts create mode 100644 src/plugins/index_pattern_field_editor/public/components/preview/is_updating_indicator.tsx create mode 100644 src/plugins/index_pattern_field_editor/public/components/preview/types.ts delete mode 100644 src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel index d2d9bf3f9a00c..caf5f7c25b569 100644 --- a/packages/kbn-monaco/BUILD.bazel +++ b/packages/kbn-monaco/BUILD.bazel @@ -36,12 +36,14 @@ RUNTIME_DEPS = [ "@npm//monaco-editor", "@npm//raw-loader", "@npm//regenerator-runtime", + "@npm//rxjs", ] TYPES_DEPS = [ "//packages/kbn-i18n", "@npm//antlr4ts", "@npm//monaco-editor", + "@npm//rxjs", "@npm//@types/jest", "@npm//@types/node", ] diff --git a/packages/kbn-monaco/__jest__/jest.mocks.ts b/packages/kbn-monaco/__jest__/jest.mocks.ts new file mode 100644 index 0000000000000..a210d7e171aa8 --- /dev/null +++ b/packages/kbn-monaco/__jest__/jest.mocks.ts @@ -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. + */ + +import { MockIModel } from './types'; + +const createMockModel = (ID: string) => { + const model: MockIModel = { + uri: '', + id: 'mockModel', + value: '', + getModeId: () => ID, + changeContentListeners: [], + setValue(newValue) { + this.value = newValue; + this.changeContentListeners.forEach((listener) => listener()); + }, + getValue() { + return this.value; + }, + onDidChangeContent(handler) { + this.changeContentListeners.push(handler); + }, + onDidChangeLanguage: (handler) => { + handler({ newLanguage: ID }); + }, + }; + + return model; +}; + +jest.mock('../src/monaco_imports', () => { + const original = jest.requireActual('../src/monaco_imports'); + const originalMonaco = original.monaco; + const originalEditor = original.monaco.editor; + + return { + ...original, + monaco: { + ...originalMonaco, + editor: { + ...originalEditor, + model: null, + createModel(ID: string) { + this.model = createMockModel(ID); + return this.model; + }, + onDidCreateModel(handler: (model: MockIModel) => void) { + if (!this.model) { + throw new Error( + `Model needs to be created by calling monaco.editor.createModel(ID) first.` + ); + } + handler(this.model); + }, + getModel() { + return this.model; + }, + getModels: () => [], + setModelMarkers: () => undefined, + }, + }, + }; +}); diff --git a/packages/kbn-monaco/__jest__/types.ts b/packages/kbn-monaco/__jest__/types.ts new file mode 100644 index 0000000000000..929964c5300fc --- /dev/null +++ b/packages/kbn-monaco/__jest__/types.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. + */ + +export interface MockIModel { + uri: string; + id: string; + value: string; + changeContentListeners: Array<() => void>; + getModeId: () => string; + setValue: (value: string) => void; + getValue: () => string; + onDidChangeContent: (handler: () => void) => void; + onDidChangeLanguage: (handler: (options: { newLanguage: string }) => void) => void; +} diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.test.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.test.ts new file mode 100644 index 0000000000000..7c3a9b66a82e1 --- /dev/null +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.test.ts @@ -0,0 +1,147 @@ +/* + * 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__/jest.mocks'; // Make sure this is the first import + +import { Subscription } from 'rxjs'; + +import { MockIModel } from '../../__jest__/types'; +import { LangValidation } from '../types'; +import { monaco } from '../monaco_imports'; +import { ID } from './constants'; + +import { DiagnosticsAdapter } from './diagnostics_adapter'; + +const getSyntaxErrors = jest.fn(async (): Promise => undefined); + +const getMockWorker = async () => { + return { + getSyntaxErrors, + } as any; +}; + +function flushPromises() { + return new Promise((resolve) => setImmediate(resolve)); +} + +describe('Painless DiagnosticAdapter', () => { + let diagnosticAdapter: DiagnosticsAdapter; + let subscription: Subscription; + let model: MockIModel; + let validation: LangValidation; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + model = monaco.editor.createModel(ID) as unknown as MockIModel; + diagnosticAdapter = new DiagnosticsAdapter(getMockWorker); + + // validate() has a promise we need to wait for + // --> await worker.getSyntaxErrors() + await flushPromises(); + + subscription = diagnosticAdapter.validation$.subscribe((newValidation) => { + validation = newValidation; + }); + }); + + afterEach(() => { + if (subscription) { + subscription.unsubscribe(); + } + }); + + test('should validate when the content changes', async () => { + expect(validation!.isValidating).toBe(false); + + model.setValue('new content'); + await flushPromises(); + expect(validation!.isValidating).toBe(true); + + jest.advanceTimersByTime(500); // there is a 500ms debounce for the validate() to trigger + await flushPromises(); + + expect(validation!.isValidating).toBe(false); + + model.setValue('changed'); + // Flushing promise here is not actually required but adding it to make sure the test + // works as expected even when doing so. + await flushPromises(); + expect(validation!.isValidating).toBe(true); + + // when we clear the content we immediately set the + // "isValidating" to false and mark the content as valid. + // No need to wait for the setTimeout + model.setValue(''); + await flushPromises(); + expect(validation!.isValidating).toBe(false); + expect(validation!.isValid).toBe(true); + }); + + test('should prevent race condition of multiple content change and validation triggered', async () => { + const errors = ['Syntax error returned']; + + getSyntaxErrors.mockResolvedValueOnce(errors); + + expect(validation!.isValidating).toBe(false); + + model.setValue('foo'); + jest.advanceTimersByTime(300); // only 300ms out of the 500ms + + model.setValue('bar'); // This will cancel the first setTimeout + + jest.advanceTimersByTime(300); // Again, only 300ms out of the 500ms. + await flushPromises(); + + expect(validation!.isValidating).toBe(true); // we are still validating + + jest.advanceTimersByTime(200); // rest of the 500ms + await flushPromises(); + + expect(validation!.isValidating).toBe(false); + expect(validation!.isValid).toBe(false); + expect(validation!.errors).toBe(errors); + }); + + test('should prevent race condition (2) of multiple content change and validation triggered', async () => { + const errors1 = ['First error returned']; + const errors2 = ['Second error returned']; + + getSyntaxErrors + .mockResolvedValueOnce(errors1) // first call + .mockResolvedValueOnce(errors2); // second call + + model.setValue('foo'); + // By now we are waiting on the worker to await getSyntaxErrors() + // we won't flush the promise to not pass this point in time just yet + jest.advanceTimersByTime(700); + + // We change the value at the same moment + model.setValue('bar'); + // now we pass the await getSyntaxErrors() point but its result (errors1) should be stale and discarted + await flushPromises(); + + jest.advanceTimersByTime(300); + await flushPromises(); + + expect(validation!.isValidating).toBe(true); // we are still validating value "bar" + + jest.advanceTimersByTime(200); // rest of the 500ms + await flushPromises(); + + expect(validation!.isValidating).toBe(false); + expect(validation!.isValid).toBe(false); + // We have the second error response, the first one has been discarted + expect(validation!.errors).toBe(errors2); + }); +}); diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts index 3d13d76743dbc..a113adb74f22d 100644 --- a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts @@ -6,7 +6,10 @@ * Side Public License, v 1. */ +import { BehaviorSubject } from 'rxjs'; + import { monaco } from '../monaco_imports'; +import { SyntaxErrors, LangValidation } from '../types'; import { ID } from './constants'; import { WorkerAccessor } from './language'; import { PainlessError } from './worker'; @@ -18,11 +21,17 @@ const toDiagnostics = (error: PainlessError): monaco.editor.IMarkerData => { }; }; -export interface SyntaxErrors { - [modelId: string]: PainlessError[]; -} export class DiagnosticsAdapter { private errors: SyntaxErrors = {}; + private validation = new BehaviorSubject({ + isValid: true, + isValidating: false, + errors: [], + }); + // To avoid stale validation data we keep track of the latest call to validate(). + private validateIdx = 0; + + public validation$ = this.validation.asObservable(); constructor(private worker: WorkerAccessor) { const onModelAdd = (model: monaco.editor.IModel): void => { @@ -35,14 +44,27 @@ export class DiagnosticsAdapter { return; } + const idx = ++this.validateIdx; // Disable any possible inflight validation + clearTimeout(handle); + // Reset the model markers if an empty string is provided on change if (model.getValue().trim() === '') { + this.validation.next({ + isValid: true, + isValidating: false, + errors: [], + }); return monaco.editor.setModelMarkers(model, ID, []); } + this.validation.next({ + ...this.validation.value, + isValidating: true, + }); // Every time a new change is made, wait 500ms before validating - clearTimeout(handle); - handle = setTimeout(() => this.validate(model.uri), 500); + handle = setTimeout(() => { + this.validate(model.uri, idx); + }, 500); }); model.onDidChangeLanguage(({ newLanguage }) => { @@ -51,21 +73,33 @@ export class DiagnosticsAdapter { if (newLanguage !== ID) { return monaco.editor.setModelMarkers(model, ID, []); } else { - this.validate(model.uri); + this.validate(model.uri, ++this.validateIdx); } }); - this.validate(model.uri); + this.validation.next({ + ...this.validation.value, + isValidating: true, + }); + this.validate(model.uri, ++this.validateIdx); } }; monaco.editor.onDidCreateModel(onModelAdd); monaco.editor.getModels().forEach(onModelAdd); } - private async validate(resource: monaco.Uri): Promise { + private async validate(resource: monaco.Uri, idx: number): Promise { + if (idx !== this.validateIdx) { + return; + } + const worker = await this.worker(resource); const errorMarkers = await worker.getSyntaxErrors(resource.toString()); + if (idx !== this.validateIdx) { + return; + } + if (errorMarkers) { const model = monaco.editor.getModel(resource); this.errors = { @@ -75,6 +109,9 @@ export class DiagnosticsAdapter { // Set the error markers and underline them with "Error" severity monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics)); } + + const isValid = errorMarkers === undefined || errorMarkers.length === 0; + this.validation.next({ isValidating: false, isValid, errors: errorMarkers ?? [] }); } public getSyntaxErrors() { diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts index 3bba7643e28b6..793dc5142a41e 100644 --- a/packages/kbn-monaco/src/painless/index.ts +++ b/packages/kbn-monaco/src/painless/index.ts @@ -8,7 +8,7 @@ import { ID } from './constants'; import { lexerRules, languageConfiguration } from './lexer_rules'; -import { getSuggestionProvider, getSyntaxErrors } from './language'; +import { getSuggestionProvider, getSyntaxErrors, validation$ } from './language'; import { CompleteLangModuleType } from '../types'; export const PainlessLang: CompleteLangModuleType = { @@ -17,6 +17,7 @@ export const PainlessLang: CompleteLangModuleType = { lexerRules, languageConfiguration, getSyntaxErrors, + validation$, }; export * from './types'; diff --git a/packages/kbn-monaco/src/painless/language.ts b/packages/kbn-monaco/src/painless/language.ts index 3cb26d970fc7d..abeee8d501f31 100644 --- a/packages/kbn-monaco/src/painless/language.ts +++ b/packages/kbn-monaco/src/painless/language.ts @@ -5,15 +5,16 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { Observable, of } from 'rxjs'; import { monaco } from '../monaco_imports'; import { WorkerProxyService, EditorStateService } from './lib'; +import { LangValidation, SyntaxErrors } from '../types'; import { ID } from './constants'; import { PainlessContext, PainlessAutocompleteField } from './types'; import { PainlessWorker } from './worker'; import { PainlessCompletionAdapter } from './completion_adapter'; -import { DiagnosticsAdapter, SyntaxErrors } from './diagnostics_adapter'; +import { DiagnosticsAdapter } from './diagnostics_adapter'; const workerProxyService = new WorkerProxyService(); const editorStateService = new EditorStateService(); @@ -37,9 +38,13 @@ let diagnosticsAdapter: DiagnosticsAdapter; // Returns syntax errors for all models by model id export const getSyntaxErrors = (): SyntaxErrors => { - return diagnosticsAdapter.getSyntaxErrors(); + return diagnosticsAdapter?.getSyntaxErrors() ?? {}; }; +export const validation$: () => Observable = () => + diagnosticsAdapter?.validation$ || + of({ isValid: true, isValidating: false, errors: [] }); + monaco.languages.onLanguage(ID, async () => { workerProxyService.setup(); diff --git a/packages/kbn-monaco/src/types.ts b/packages/kbn-monaco/src/types.ts index 0e20021bf69eb..8512ef1ac58c0 100644 --- a/packages/kbn-monaco/src/types.ts +++ b/packages/kbn-monaco/src/types.ts @@ -5,6 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { Observable } from 'rxjs'; + import { monaco } from './monaco_imports'; export interface LangModuleType { @@ -19,4 +21,23 @@ export interface CompleteLangModuleType extends LangModuleType { languageConfiguration: monaco.languages.LanguageConfiguration; getSuggestionProvider: Function; getSyntaxErrors: Function; + validation$: () => Observable; +} + +export interface EditorError { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + message: string; +} + +export interface LangValidation { + isValidating: boolean; + isValid: boolean; + errors: EditorError[]; +} + +export interface SyntaxErrors { + [modelId: string]: EditorError[]; } diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index 959051b17b782..4a373843c555a 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -14,5 +14,6 @@ }, "include": [ "src/**/*", + "__jest__/**/*", ] } diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/use_async_validation_data.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/use_async_validation_data.mdx deleted file mode 100644 index 8020a54596b46..0000000000000 --- a/src/plugins/es_ui_shared/static/forms/docs/core/use_async_validation_data.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: formLibCoreUseAsyncValidationData -slug: /form-lib/core/use-async-validation-data -title: useAsyncValidationData() -summary: Provide dynamic data to your validators... asynchronously -tags: ['forms', 'kibana', 'dev'] -date: 2021-08-20 ---- - -**Returns:** `[Observable, (nextValue: T|undefined) => void]` - -This hook creates for you an observable and a handler to update its value. You can then pass the observable directly to . - -See an example on how to use this hook in the section. - -## Options - -### state (optional) - -**Type:** `any` - -If you provide a state when calling the hook, the observable value will keep in sync with the state. - -```js -const MyForm = () => { - ... - const [indices, setIndices] = useState([]); - // Whenever the "indices" state changes, the "indices$" Observable will be updated - const [indices$] = useAsyncValidationData(indices); - - ... - - - -} -``` \ No newline at end of file diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/use_behavior_subject.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/use_behavior_subject.mdx new file mode 100644 index 0000000000000..f7eca9c360ac4 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/docs/core/use_behavior_subject.mdx @@ -0,0 +1,26 @@ +--- +id: formLibCoreUseBehaviorSubject +slug: /form-lib/utils/use-behavior-subject +title: useBehaviorSubject() +summary: Util to create a rxjs BehaviorSubject with a handler to change its value +tags: ['forms', 'kibana', 'dev'] +date: 2021-08-20 +--- + +**Returns:** `[Observable, (nextValue: T|undefined) => void]` + +This hook creates for you a rxjs BehaviorSubject and a handler to update its value. + +See an example on how to use this hook in the section. + +## Options + +### initialState + +**Type:** `any` + +The initial value of the BehaviorSubject. + +```js +const [indices$, nextIndices] = useBehaviorSubject([]); +``` \ No newline at end of file diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/use_field.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/use_field.mdx index fd5f3b26cdf0d..dd073e0b38d1f 100644 --- a/src/plugins/es_ui_shared/static/forms/docs/core/use_field.mdx +++ b/src/plugins/es_ui_shared/static/forms/docs/core/use_field.mdx @@ -207,6 +207,14 @@ For example: when we add an item to the ComboBox array, we don't want to block t By default, when any of the validation fails, the following validation are not executed. If you still want to execute the following validation(s), set the `exitOnFail` to `false`. +##### isAsync + +**Type:** `boolean` +**Default:** `false` + +Flag to indicate if the validation is asynchronous. If not specified the lib will first try to run all the validations synchronously and if it detects a Promise it will run the validations a second time asynchronously. This means that HTTP request will be called twice which is not ideal. +**It is thus recommended** to set the `isAsync` flag to `true` for all asynchronous validations. + #### deserializer **Type:** `SerializerFunc` @@ -342,9 +350,9 @@ Use this prop to pass down dynamic data to your field validator. The data is the See an example on how to use this prop in the section. -### validationData$ +### validationDataProvider -Use this prop to pass down an Observable into which you can send, asynchronously, dynamic data required inside your validation. +Use this prop to pass down a Promise to provide dynamic data asynchronously in your validation. See an example on how to use this prop in the section. diff --git a/src/plugins/es_ui_shared/static/forms/docs/core/use_form_data.mdx b/src/plugins/es_ui_shared/static/forms/docs/core/use_form_data.mdx index 17276f41b3dac..0deb449591871 100644 --- a/src/plugins/es_ui_shared/static/forms/docs/core/use_form_data.mdx +++ b/src/plugins/es_ui_shared/static/forms/docs/core/use_form_data.mdx @@ -56,6 +56,31 @@ const [{ type }] = useFormData({ watch: 'type' }); const [{ type, subType }] = useFormData({ watch: ['type', 'subType'] }); ``` +### onChange + +**Type:** `(data: T) => void` + +This handler lets you listen to form fields value change _before_ any validation is executed. + +```js +// With "onChange": listen to changes before any validation is triggered +const onFieldChange = useCallback(({ myField, otherField }) => { + // React to changes before any validation is executed +}, []); + +useFormData({ + watch: ['myField', 'otherField'], + onChange: onFieldChange +}); + +// Without "onChange": the way to go most of the time +const [{ myField, otherField }] = useFormData({ watch['myField', 'otherField'] }); + +useEffect(() => { + // React to changes after validation have been triggered +}, [myField, otherField]); +``` + ## Return As you have noticed, you get back an array from the hook. The first element of the array is form data and the second argument is a handler to get the **serialized** form data if needed. diff --git a/src/plugins/es_ui_shared/static/forms/docs/examples/validation.mdx b/src/plugins/es_ui_shared/static/forms/docs/examples/validation.mdx index 8526a8912ba08..43ec8da11c5cc 100644 --- a/src/plugins/es_ui_shared/static/forms/docs/examples/validation.mdx +++ b/src/plugins/es_ui_shared/static/forms/docs/examples/validation.mdx @@ -334,7 +334,7 @@ const MyForm = () => { Great. Now let's imagine that you want to add a validation to the `indexName` field and mark it as invalid if it does not match at least one index in the cluster. For that you need to provide dynamic data (the list of indices fetched) which is not immediately accesible when the field value changes (and the validation kicks in). We need to ask the validation to **wait** until we have fetched the indices and then have access to the dynamic data. -For that we will use the `validationData$` Observable that you can pass to the field. Whenever a value is sent to the observable (**after** the field value has changed, important!), it will be available in the validator through the `customData.provider()` handler. +For that we will use the `validationDataProvider` prop that you can pass to the field. This data provider will be available in the validator through the `customData.provider()` handler. ```js // form.schema.ts @@ -357,15 +357,28 @@ const schema = { } // myform.tsx +import { firstValueFrom } from '@kbn/std'; + const MyForm = () => { ... const [indices, setIndices] = useState([]); - const [indices$, nextIndices] = useAsyncValidationData(); // Use the provided hook to create the Observable + const [indices$, nextIndices] = useBehaviorSubject(null); // Use the provided util hook to create an observable + + const indicesProvider = useCallback(() => { + // We wait until we have fetched the indices. + // The result will then be sent to the validator (await provider() call); + return await firstValueFrom(indices$.pipe(first((data) => data !== null))); + }, [indices$, nextIndices]); const fetchIndices = useCallback(async () => { + // Reset the subject to not send stale data to the validator + nextIndices(null); + const result = await httpClient.get(`/api/search/${indexName}`); setIndices(result); - nextIndices(result); // Send the indices to your validator "provider()" + + // Send the indices to the BehaviorSubject to resolve the validator "provider()" + nextIndices(result); }, [indexName]); // Whenever the indexName changes we fetch the indices @@ -377,7 +390,7 @@ const MyForm = () => { <> /* Pass the Observable to your field */ - + ... diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index 0950f2dabb1b7..cbf0d9d619636 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import React, { useEffect, FunctionComponent, useState } from 'react'; +import React, { useEffect, FunctionComponent, useState, useCallback } from 'react'; import { act } from 'react-dom/test-utils'; +import { first } from 'rxjs/operators'; import { registerTestBed, TestBed } from '../shared_imports'; import { FormHook, OnUpdateHandler, FieldConfig, FieldHook } from '../types'; import { useForm } from '../hooks/use_form'; -import { useAsyncValidationData } from '../hooks/use_async_validation_data'; +import { useBehaviorSubject } from '../hooks/utils/use_behavior_subject'; import { Form } from './form'; import { UseField } from './use_field'; @@ -420,8 +421,18 @@ describe('', () => { const TestComp = ({ validationData }: DynamicValidationDataProps) => { const { form } = useForm({ schema }); - const [stateValue, setStateValue] = useState('initialValue'); - const [validationData$, next] = useAsyncValidationData(stateValue); + const [validationData$, next] = useBehaviorSubject(undefined); + + const validationDataProvider = useCallback(async () => { + const data = await validationData$ + .pipe(first((value) => value !== undefined)) + .toPromise(); + + // Clear the Observable so we are forced to send a new value to + // resolve the provider + next(undefined); + return data; + }, [validationData$, next]); const setInvalidDynamicData = () => { next('bad'); @@ -431,22 +442,12 @@ describe('', () => { next('good'); }; - // Updating the state should emit a new value in the observable - // which in turn should be available in the validation and allow it to complete. - const setStateValueWithValidValue = () => { - setStateValue('good'); - }; - - const setStateValueWithInValidValue = () => { - setStateValue('bad'); - }; - return (
    <> {/* Dynamic async validation data with an observable. The validation will complete **only after** the observable has emitted a value. */} - path="name" validationData$={validationData$}> + path="name" validationDataProvider={validationDataProvider}> {(field) => { onNameFieldHook(field); return ( @@ -479,15 +480,6 @@ describe('', () => { - - ); @@ -519,7 +511,8 @@ describe('', () => { await act(async () => { jest.advanceTimersByTime(10000); }); - // The field is still validating as no value has been sent to the observable + // The field is still validating as the validationDataProvider has not resolved yet + // (no value has been sent to the observable) expect(nameFieldHook?.isValidating).toBe(true); // We now send a valid value to the observable @@ -545,38 +538,6 @@ describe('', () => { expect(nameFieldHook?.getErrorsMessages()).toBe('Invalid dynamic data'); }); - test('it should access dynamic data coming after the field value changed, **in sync** with a state change', async () => { - const { form, find } = setupDynamicData(); - - await act(async () => { - form.setInputValue('nameField', 'newValue'); - }); - expect(nameFieldHook?.isValidating).toBe(true); - - // We now update the state with a valid value - // this should update the observable - await act(async () => { - find('setValidStateValueBtn').simulate('click'); - }); - - expect(nameFieldHook?.isValidating).toBe(false); - expect(nameFieldHook?.isValid).toBe(true); - - // Let's change the input value to trigger the validation once more - await act(async () => { - form.setInputValue('nameField', 'anotherValue'); - }); - expect(nameFieldHook?.isValidating).toBe(true); - - // And change the state with an invalid value - await act(async () => { - find('setInvalidStateValueBtn').simulate('click'); - }); - - expect(nameFieldHook?.isValidating).toBe(false); - expect(nameFieldHook?.isValid).toBe(false); - }); - test('it should access dynamic data provided through props', async () => { let { form } = setupDynamicData({ validationData: 'good' }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx index a73eee1bd8bd3..49ee21667752a 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -7,7 +7,6 @@ */ import React, { FunctionComponent } from 'react'; -import { Observable } from 'rxjs'; import { FieldHook, FieldConfig, FormData } from '../types'; import { useField } from '../hooks'; @@ -23,8 +22,6 @@ export interface Props { /** * Use this prop to pass down dynamic data **asynchronously** to your validators. * Your validator accesses the dynamic data by resolving the provider() Promise. - * The Promise will resolve **when a new value is sent** to the validationData$ Observable. - * * ```typescript * validator: ({ customData }) => { * // Wait until a value is sent to the "validationData$" Observable @@ -32,7 +29,7 @@ export interface Props { * } * ``` */ - validationData$?: Observable; + validationDataProvider?: () => Promise; /** * Use this prop to pass down dynamic data to your validators. The validation data * is then accessible in your validator inside the `customData.value` property. @@ -63,7 +60,7 @@ function UseFieldComp(props: Props(props: Props(form, path, fieldConfig, onChange, onError, { - customValidationData$, customValidationData, + customValidationDataProvider, }); // Children prevails over anything else provided. diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts index 6f2dc768508ec..f4911bfaadfa4 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -11,4 +11,4 @@ export { useField } from './use_field'; export { useForm } from './use_form'; export { useFormData } from './use_form_data'; export { useFormIsModified } from './use_form_is_modified'; -export { useAsyncValidationData } from './use_async_validation_data'; +export { useBehaviorSubject } from './utils'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_async_validation_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_async_validation_data.ts deleted file mode 100644 index 21d5e101536ae..0000000000000 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_async_validation_data.ts +++ /dev/null @@ -1,36 +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 { useCallback, useRef, useMemo, useEffect } from 'react'; -import { Subject, Observable } from 'rxjs'; - -export const useAsyncValidationData = (state?: T) => { - const validationData$ = useRef>(); - - const getValidationData$ = useCallback(() => { - if (validationData$.current === undefined) { - validationData$.current = new Subject(); - } - return validationData$.current; - }, []); - - const hook: [Observable, (value?: T) => void] = useMemo(() => { - const subject = getValidationData$(); - - const observable = subject.asObservable(); - const next = subject.next.bind(subject); - - return [observable, next]; - }, [getValidationData$]); - - // Whenever the state changes we update the observable - useEffect(() => { - getValidationData$().next(state); - }, [state, getValidationData$]); - - return hook; -}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index c01295f6ee42c..5079a8b69ba80 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -7,8 +7,6 @@ */ import { useMemo, useState, useEffect, useRef, useCallback } from 'react'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { FormHook, @@ -33,9 +31,12 @@ export const useField = ( valueChangeListener?: (value: I) => void, errorChangeListener?: (errors: string[] | null) => void, { - customValidationData$, customValidationData = null, - }: { customValidationData$?: Observable; customValidationData?: unknown } = {} + customValidationDataProvider, + }: { + customValidationData?: unknown; + customValidationDataProvider?: () => Promise; + } = {} ) => { const { type = FIELD_TYPES.TEXT, @@ -59,7 +60,7 @@ export const useField = ( __addField, __removeField, __updateFormDataAt, - __validateFields, + validateFields, __getFormData$, } = form; @@ -94,6 +95,14 @@ export const useField = ( errors: null, }); + const hasAsyncValidation = useMemo( + () => + validations === undefined + ? false + : validations.some((validation) => validation.isAsync === true), + [validations] + ); + // ---------------------------------- // -- HELPERS // ---------------------------------- @@ -147,7 +156,7 @@ export const useField = ( __updateFormDataAt(path, value); // Validate field(s) (this will update the form.isValid state) - await __validateFields(fieldsToValidateOnChange ?? [path]); + await validateFields(fieldsToValidateOnChange ?? [path]); if (isMounted.current === false) { return; @@ -156,7 +165,7 @@ export const useField = ( /** * If we have set a delay to display the error message after the field value has changed, * we first check that this is the last "change iteration" (=== the last keystroke from the user) - * and then, we verify how long we've already waited for as form.__validateFields() is asynchronous + * and then, we verify how long we've already waited for as form.validateFields() is asynchronous * and might already have taken more than the specified delay) */ if (changeIteration === changeCounter.current) { @@ -181,7 +190,7 @@ export const useField = ( valueChangeDebounceTime, fieldsToValidateOnChange, __updateFormDataAt, - __validateFields, + validateFields, ]); // Cancel any inflight validation (e.g an HTTP Request) @@ -238,18 +247,13 @@ export const useField = ( return false; }; - let dataProvider: () => Promise = () => Promise.resolve(null); - - if (customValidationData$) { - dataProvider = () => customValidationData$.pipe(first()).toPromise(); - } + const dataProvider: () => Promise = + customValidationDataProvider ?? (() => Promise.resolve(undefined)); const runAsync = async () => { const validationErrors: ValidationError[] = []; for (const validation of validations) { - inflightValidation.current = null; - const { validator, exitOnFail = true, @@ -271,6 +275,8 @@ export const useField = ( const validationResult = await inflightValidation.current; + inflightValidation.current = null; + if (!validationResult) { continue; } @@ -345,17 +351,22 @@ export const useField = ( return validationErrors; }; + if (hasAsyncValidation) { + return runAsync(); + } + // We first try to run the validations synchronously return runSync(); }, [ cancelInflightValidation, validations, + hasAsyncValidation, getFormData, getFields, path, customValidationData, - customValidationData$, + customValidationDataProvider, ] ); @@ -388,7 +399,6 @@ export const useField = ( onlyBlocking = false, } = validationData; - setIsValidated(true); setValidating(true); // By the time our validate function has reached completion, it’s possible @@ -401,6 +411,7 @@ export const useField = ( if (validateIteration === validateCounter.current && isMounted.current) { // This is the most recent invocation setValidating(false); + setIsValidated(true); // Update the errors array setStateErrors((prev) => { const filteredErrors = filterErrors(prev, validationType); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 92a9876f1cd30..e3e818729340e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -572,4 +572,63 @@ describe('useForm() hook', () => { expect(isValid).toBe(false); }); }); + + describe('form.getErrors()', () => { + test('should return the errors in the form', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
    + + + { + if (value === 'bad') { + return { + message: 'Field2 is invalid', + }; + } + }, + }, + ], + }} + /> + + ); + }; + + const { + form: { setInputValue }, + } = registerTestBed(TestComp)() as TestBed; + + let errors: string[] = formHook!.getErrors(); + expect(errors).toEqual([]); + + await act(async () => { + await formHook!.submit(); + }); + errors = formHook!.getErrors(); + expect(errors).toEqual(['Field1 can not be empty']); + + await setInputValue('field2', 'bad'); + errors = formHook!.getErrors(); + expect(errors).toEqual(['Field1 can not be empty', 'Field2 is invalid']); + }); + }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 23827c0d1aa3b..f8a773597a823 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -66,6 +66,7 @@ export function useForm( const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setSubmitting] = useState(false); const [isValid, setIsValid] = useState(undefined); + const [errorMessages, setErrorMessages] = useState<{ [fieldName: string]: string }>({}); const fieldsRefs = useRef({}); const fieldsRemovedRefs = useRef({}); @@ -73,6 +74,19 @@ export function useForm( const isMounted = useRef(false); const defaultValueDeserialized = useRef(defaultValueMemoized); + /** + * We have both a state and a ref for the error messages so the consumer can, in the same callback, + * validate the form **and** have the errors returned immediately. + * + * ``` + * const myHandler = useCallback(async () => { + * const isFormValid = await validate(); + * const errors = getErrors(); // errors from the validate() call are there + * }, [validate, getErrors]); + * ``` + */ + const errorMessagesRef = useRef<{ [fieldName: string]: string }>({}); + // formData$ is an observable we can subscribe to in order to receive live // update of the raw form data. As an observable it does not trigger any React // render(). @@ -97,6 +111,34 @@ export function useForm( [getFormData$] ); + const updateFieldErrorMessage = useCallback((path: string, errorMessage: string | null) => { + setErrorMessages((prev) => { + const previousMessageValue = prev[path]; + + if ( + errorMessage === previousMessageValue || + (previousMessageValue === undefined && errorMessage === null) + ) { + // Don't update the state, the error message has not changed. + return prev; + } + + if (errorMessage === null) { + // We strip out previous error message + const { [path]: discard, ...next } = prev; + errorMessagesRef.current = next; + return next; + } + + const next = { + ...prev, + [path]: errorMessage, + }; + errorMessagesRef.current = next; + return next; + }); + }, []); + const fieldsToArray = useCallback<() => FieldHook[]>(() => Object.values(fieldsRefs.current), []); const getFieldsForOutput = useCallback( @@ -158,7 +200,7 @@ export function useForm( }); }, [fieldsToArray]); - const validateFields: FormHook['__validateFields'] = useCallback( + const validateFields: FormHook['validateFields'] = useCallback( async (fieldNames, onlyBlocking = false) => { const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) @@ -224,6 +266,7 @@ export function useForm( delete fieldsRemovedRefs.current[field.path]; updateFormDataAt(field.path, field.value); + updateFieldErrorMessage(field.path, field.getErrorsMessages()); if (!fieldExists && !field.isValidated) { setIsValid(undefined); @@ -235,7 +278,7 @@ export function useForm( setIsSubmitted(false); } }, - [updateFormDataAt] + [updateFormDataAt, updateFieldErrorMessage] ); const removeField: FormHook['__removeField'] = useCallback( @@ -247,7 +290,7 @@ export function useForm( // Keep a track of the fields that have been removed from the form // This will allow us to know if the form has been modified fieldsRemovedRefs.current[name] = fieldsRefs.current[name]; - + updateFieldErrorMessage(name, null); delete fieldsRefs.current[name]; delete currentFormData[name]; }); @@ -267,7 +310,7 @@ export function useForm( return prev; }); }, - [getFormData$, updateFormData$, fieldsToArray] + [getFormData$, updateFormData$, fieldsToArray, updateFieldErrorMessage] ); const getFormDefaultValue: FormHook['__getFormDefaultValue'] = useCallback( @@ -306,15 +349,8 @@ export function useForm( if (isValid === true) { return []; } - - return fieldsToArray().reduce((acc, field) => { - const fieldError = field.getErrorsMessages(); - if (fieldError === null) { - return acc; - } - return [...acc, fieldError]; - }, [] as string[]); - }, [isValid, fieldsToArray]); + return Object.values({ ...errorMessages, ...errorMessagesRef.current }); + }, [isValid, errorMessages]); const validate: FormHook['validate'] = useCallback(async (): Promise => { // Maybe some field are being validated because of their async validation(s). @@ -458,6 +494,7 @@ export function useForm( getFormData, getErrors, reset, + validateFields, __options: formOptions, __getFormData$: getFormData$, __updateFormDataAt: updateFormDataAt, @@ -467,7 +504,6 @@ export function useForm( __addField: addField, __removeField: removeField, __getFieldsRemoved: getFieldsRemoved, - __validateFields: validateFields, }; }, [ isSubmitted, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx index c6f920ef88c69..614d4a5f3fd1d 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -15,7 +15,7 @@ import { useForm } from './use_form'; import { useFormData, HookReturn } from './use_form_data'; interface Props { - onChange(data: HookReturn): void; + onHookValueChange(data: HookReturn): void; watch?: string | string[]; } @@ -36,16 +36,16 @@ interface Form3 { } describe('useFormData() hook', () => { - const HookListenerComp = function ({ onChange, watch }: Props) { + const HookListenerComp = function ({ onHookValueChange, watch }: Props) { const hookValue = useFormData({ watch }); const isMounted = useRef(false); useEffect(() => { if (isMounted.current) { - onChange(hookValue); + onHookValueChange(hookValue); } isMounted.current = true; - }, [hookValue, onChange]); + }, [hookValue, onHookValueChange]); return null; }; @@ -77,7 +77,7 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - testBed = setup({ onChange: onChangeSpy }) as TestBed; + testBed = setup({ onHookValueChange: onChangeSpy }) as TestBed; }); test('should return the form data', () => { @@ -126,7 +126,7 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - setup({ onChange: onChangeSpy }); + setup({ onHookValueChange: onChangeSpy }); }); test('should expose a handler to build the form data', () => { @@ -171,7 +171,7 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - testBed = setup({ watch: 'title', onChange: onChangeSpy }) as TestBed; + testBed = setup({ watch: 'title', onHookValueChange: onChangeSpy }) as TestBed; }); test('should not listen to changes on fields we are not interested in', async () => { @@ -199,13 +199,13 @@ describe('useFormData() hook', () => { return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; }; - const TestComp = ({ onChange }: Props) => { + const TestComp = ({ onHookValueChange }: Props) => { const { form } = useForm(); const hookValue = useFormData({ form }); useEffect(() => { - onChange(hookValue); - }, [hookValue, onChange]); + onHookValueChange(hookValue); + }, [hookValue, onHookValueChange]); return (
    @@ -220,7 +220,7 @@ describe('useFormData() hook', () => { beforeEach(() => { onChangeSpy = jest.fn(); - testBed = setup({ onChange: onChangeSpy }) as TestBed; + testBed = setup({ onHookValueChange: onChangeSpy }) as TestBed; }); test('should allow a form to be provided when the hook is called outside of the FormDataContext', async () => { @@ -239,5 +239,71 @@ describe('useFormData() hook', () => { expect(updatedData).toEqual({ title: 'titleChanged' }); }); }); + + describe('onChange', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + let validationSpy: jest.Mock; + + const TestComp = () => { + const { form } = useForm(); + useFormData({ form, onChange: onChangeSpy }); + + return ( + + { + // This spy should be called **after** the onChangeSpy + validationSpy(); + }, + }, + ], + }} + /> + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + validationSpy = jest.fn(); + testBed = setup({ watch: 'title' }) as TestBed; + }); + + test('should call onChange handler _before_ running the validations', async () => { + const { + form: { setInputValue }, + } = testBed; + + onChangeSpy.mockReset(); // Reset our counters + validationSpy.mockReset(); + + expect(onChangeSpy).not.toHaveBeenCalled(); + expect(validationSpy).not.toHaveBeenCalled(); + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + expect(onChangeSpy).toHaveBeenCalled(); + expect(validationSpy).toHaveBeenCalled(); + + const onChangeCallOrder = onChangeSpy.mock.invocationCallOrder[0]; + const validationCallOrder = validationSpy.mock.invocationCallOrder[0]; + + // onChange called before validation + expect(onChangeCallOrder).toBeLessThan(validationCallOrder); + }); + }); }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts index 7ad98bc2483bb..7185421553bbf 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -6,23 +6,28 @@ * Side Public License, v 1. */ -import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { FormData, FormHook } from '../types'; import { unflattenObject } from '../lib'; import { useFormDataContext, Context } from '../form_data_context'; -interface Options { +interface Options { watch?: string | string[]; form?: FormHook; + /** + * Use this handler if you want to listen to field value change + * before the validations are ran. + */ + onChange?: (formData: I) => void; } export type HookReturn = [I, () => T, boolean]; export const useFormData = ( - options: Options = {} + options: Options = {} ): HookReturn => { - const { watch, form } = options; + const { watch, form, onChange } = options; const ctx = useFormDataContext(); const watchToArray: string[] = watch === undefined ? [] : Array.isArray(watch) ? watch : [watch]; // We will use "stringifiedWatch" to compare if the array has changed in the useMemo() below @@ -57,29 +62,38 @@ export const useFormData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [getFormData, formData]); - const subscription = useMemo(() => { - return getFormData$().subscribe((raw) => { + useEffect(() => { + const subscription = getFormData$().subscribe((raw) => { if (!isMounted.current && Object.keys(raw).length === 0) { return; } if (watchToArray.length > 0) { + // Only update the state if one of the field we watch has changed. if (watchToArray.some((path) => previousRawData.current[path] !== raw[path])) { previousRawData.current = raw; - // Only update the state if one of the field we watch has changed. - setFormData(unflattenObject(raw)); + const nextState = unflattenObject(raw); + + if (onChange) { + onChange(nextState); + } + + setFormData(nextState); } } else { - setFormData(unflattenObject(raw)); + const nextState = unflattenObject(raw); + if (onChange) { + onChange(nextState); + } + setFormData(nextState); } }); - // To compare we use the stringified version of the "watchToArray" array - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stringifiedWatch, getFormData$]); - useEffect(() => { return subscription.unsubscribe; - }, [subscription]); + + // To compare we use the stringified version of the "watchToArray" array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stringifiedWatch, getFormData$, onChange]); useEffect(() => { isMounted.current = true; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/index.ts new file mode 100644 index 0000000000000..f7d3bd563ea3b --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/index.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 { useBehaviorSubject } from './use_behavior_subject'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/use_behavior_subject.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/use_behavior_subject.ts new file mode 100644 index 0000000000000..3bf4a6b225c8b --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/utils/use_behavior_subject.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 { useCallback, useRef, useMemo } from 'react'; +import { BehaviorSubject, Observable } from 'rxjs'; + +export const useBehaviorSubject = (initialState: T) => { + const subjectRef = useRef>(); + + const getSubject$ = useCallback(() => { + if (subjectRef.current === undefined) { + subjectRef.current = new BehaviorSubject(initialState); + } + return subjectRef.current; + }, [initialState]); + + const hook: [Observable, (value: T) => void] = useMemo(() => { + const subject = getSubject$(); + + const observable = subject.asObservable(); + const next = subject.next.bind(subject); + + return [observable, next]; + }, [getSubject$]); + + return hook; +}; 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 b5c7f5b4214e0..258b15e96e442 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 @@ -8,7 +8,7 @@ // 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, useAsyncValidationData } from './hooks'; +export { useForm, useFormData, useFormIsModified, useBehaviorSubject } 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 cfb211b702ed6..2e1863adaa467 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 @@ -50,15 +50,15 @@ export interface FormHook * all the fields to their initial values. */ reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; - readonly __options: Required; - __getFormData$: () => Subject; - __addField: (field: FieldHook) => void; - __removeField: (fieldNames: string | string[]) => void; - __validateFields: ( + validateFields: ( fieldNames: string[], /** Run only blocking validations */ onlyBlocking?: boolean ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>; + readonly __options: Required; + __getFormData$: () => Subject; + __addField: (field: FieldHook) => void; + __removeField: (fieldNames: string | string[]) => void; __updateFormDataAt: (field: string, value: unknown) => void; __updateDefaultValueAt: (field: string, value: unknown) => void; __readFieldConfigFromSchema: (field: string) => FieldConfig; @@ -206,7 +206,14 @@ export type ValidationFunc< V = unknown > = ( data: ValidationFuncArg -) => ValidationError | void | undefined | Promise | void | undefined>; +) => ValidationError | void | undefined | ValidationCancelablePromise; + +export type ValidationResponsePromise = Promise< + ValidationError | void | undefined +>; + +export type ValidationCancelablePromise = + ValidationResponsePromise & { cancel?(): void }; export interface FieldValidateResponse { isValid: boolean; @@ -239,4 +246,12 @@ export interface ValidationConfig< */ isBlocking?: boolean; exitOnFail?: boolean; + /** + * Flag to indicate if the validation is asynchronous. If not specified the lib will + * first try to run all the validations synchronously and if it detects a Promise it + * will run the validations a second time asynchronously. + * This means that HTTP request will be called twice which is not ideal. It is then + * recommended to set the "isAsync" flag to `true` to all asynchronous validations. + */ + isAsync?: 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 index 0d58b2ce89358..1fd280a937a03 100644 --- 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 @@ -12,12 +12,10 @@ import { Context } from '../../public/components/field_editor_context'; import { FieldEditor, Props } from '../../public/components/field_editor/field_editor'; import { WithFieldEditorDependencies, getCommonActions } from './helpers'; +export { waitForUpdates, waitForDocumentsAndPreviewUpdate } from './helpers'; + export const defaultProps: Props = { onChange: jest.fn(), - syntaxError: { - error: null, - clear: () => {}, - }, }; export type FieldEditorTestBed = TestBed & { actions: ReturnType }; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx index 4a4c42f69fc8e..55b9876ac54ad 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor.test.tsx @@ -5,20 +5,18 @@ * 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 { 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 { setupEnvironment, mockDocuments } from './helpers'; +import { 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'; +import { setSearchResponse } from './field_editor_flyout_preview.helpers'; +import { + setup, + FieldEditorTestBed, + waitForDocumentsAndPreviewUpdate, +} from './field_editor.helpers'; describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -42,18 +40,14 @@ describe('', () => { let promise: ReturnType; await act(async () => { - // We can't await for the promise here as the validation for the - // "script" field has a setTimeout which is mocked by jest. If we await - // we don't have the chance to call jest.advanceTimersByTime and thus the - // test times out. + // We can't await for the promise here ("await state.submit()") as the validation for the + // "script" field has different setTimeout mocked by jest. + // If we await here (await state.submit()) we don't have the chance to call jest.advanceTimersByTime() + // below and the test times out. promise = state.submit(); }); - await act(async () => { - // The painless syntax validation has a timeout set to 600ms - // we give it a bit more time just to be on the safe side - jest.advanceTimersByTime(1000); - }); + await waitForDocumentsAndPreviewUpdate(); await act(async () => { promise.then((response) => { @@ -61,7 +55,13 @@ describe('', () => { }); }); - return formState!; + if (formState === undefined) { + throw new Error( + `The form state is not defined, this probably means that the promise did not resolve due to an unresolved validation.` + ); + } + + return formState; }; beforeAll(() => { @@ -75,6 +75,7 @@ describe('', () => { beforeEach(async () => { onChange = jest.fn(); + setSearchResponse(mockDocuments); httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] }); }); @@ -88,7 +89,7 @@ describe('', () => { try { expect(isOn).toBe(false); - } catch (e) { + } catch (e: any) { e.message = `"${row}" row toggle expected to be 'off' but was 'on'. \n${e.message}`; throw e; } @@ -179,74 +180,5 @@ describe('', () => { expect(getLastStateUpdate().isValid).toBe(true); expect(form.getErrorsMessages()).toEqual([]); }); - - test('should clear the painless syntax error whenever the field type changes', async () => { - const field: Field = { - name: 'myRuntimeField', - type: 'keyword', - script: { source: 'emit(6)' }, - }; - - 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]); - - return ( - <> - - - {/* Button to forward dummy syntax error */} - - - ); - }; - - let testBedToCapturePainlessErrors: TestBed; - - await act(async () => { - testBedToCapturePainlessErrors = await registerTestBed( - WithFieldEditorDependencies(ComponentToProvidePainlessSyntaxErrors), - { - memoryRouter: { - wrapComponent: false, - }, - } - )(); - }); - - testBed = { - ...testBedToCapturePainlessErrors!, - actions: getCommonActions(testBedToCapturePainlessErrors!), - }; - - const { - form, - component, - find, - actions: { fields }, - } = testBed; - - // We set some dummy painless error - act(() => { - find('setPainlessErrorButton').simulate('click'); - }); - component.update(); - - expect(form.getErrorsMessages()).toEqual(['Awwww! Painless syntax error']); - - // We change the type and expect the form error to not be there anymore - 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 index 5b916c1cd9960..0e87756819bf2 100644 --- 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 @@ -15,10 +15,11 @@ import { } from '../../public/components/field_editor_flyout_content'; import { WithFieldEditorDependencies, getCommonActions } from './helpers'; +export { waitForUpdates, waitForDocumentsAndPreviewUpdate } from './helpers'; + const defaultProps: Props = { onSave: () => {}, onCancel: () => {}, - runtimeFieldValidator: () => Promise.resolve(null), isSavingField: false, }; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts index 9b00ff762fe8f..1730593dbda20 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_content.test.ts @@ -7,15 +7,17 @@ */ import { act } from 'react-dom/test-utils'; -import type { Props } from '../../public/components/field_editor_flyout_content'; +// This import needs to come first as it contains the jest.mocks import { setupEnvironment } from './helpers'; +import type { Props } from '../../public/components/field_editor_flyout_content'; +import { setSearchResponse } from './field_editor_flyout_preview.helpers'; import { setup } from './field_editor_flyout_content.helpers'; +import { mockDocuments, createPreviewError } from './helpers/mocks'; describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); beforeAll(() => { - httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['foo'] }); jest.useFakeTimers(); }); @@ -24,6 +26,11 @@ describe('', () => { server.restore(); }); + beforeEach(async () => { + setSearchResponse(mockDocuments); + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] }); + }); + test('should have the correct title', async () => { const { exists, find } = await setup(); expect(exists('flyoutTitle')).toBe(true); @@ -55,17 +62,13 @@ describe('', () => { }; const onSave: jest.Mock = jest.fn(); - const { find } = await setup({ onSave, field }); + const { find, actions } = await setup({ onSave, field }); await act(async () => { find('fieldSaveButton').simulate('click'); }); - await act(async () => { - // The painless syntax validation has a timeout set to 600ms - // we give it a bit more time just to be on the safe side - jest.advanceTimersByTime(1000); - }); + await actions.waitForUpdates(); // Run the validations expect(onSave).toHaveBeenCalled(); const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; @@ -85,7 +88,11 @@ describe('', () => { test('should validate the fields and prevent saving invalid form', async () => { const onSave: jest.Mock = jest.fn(); - const { find, exists, form, component } = await setup({ onSave }); + const { + find, + form, + actions: { waitForUpdates }, + } = await setup({ onSave }); expect(find('fieldSaveButton').props().disabled).toBe(false); @@ -93,17 +100,11 @@ describe('', () => { find('fieldSaveButton').simulate('click'); }); - await act(async () => { - jest.advanceTimersByTime(1000); - }); - - component.update(); + await waitForUpdates(); expect(onSave).toHaveBeenCalledTimes(0); expect(find('fieldSaveButton').props().disabled).toBe(true); expect(form.getErrorsMessages()).toEqual(['A name is required.']); - expect(exists('formError')).toBe(true); - expect(find('formError').text()).toBe('Fix errors in form before continuing.'); }); test('should forward values from the form', async () => { @@ -111,17 +112,14 @@ describe('', () => { const { find, - actions: { toggleFormRow, fields }, + actions: { toggleFormRow, fields, waitForUpdates }, } = await setup({ onSave }); await fields.updateName('someName'); await toggleFormRow('value'); await fields.updateScript('echo("hello")'); - await act(async () => { - // Let's make sure that validation has finished running - jest.advanceTimersByTime(1000); - }); + await waitForUpdates(); await act(async () => { find('fieldSaveButton').simulate('click'); @@ -138,7 +136,8 @@ describe('', () => { }); // Change the type and make sure it is forwarded - await fields.updateType('other_type', 'Other type'); + await fields.updateType('date'); + await waitForUpdates(); await act(async () => { find('fieldSaveButton').simulate('click'); @@ -148,7 +147,44 @@ describe('', () => { expect(fieldReturned).toEqual({ name: 'someName', - type: 'other_type', + type: 'date', + script: { source: 'echo("hello")' }, + }); + }); + + test('should not block validation if no documents could be fetched from server', async () => { + // If no documents can be fetched from the cluster (either because there are none or because + // the request failed), we still need to be able to resolve the painless script validation. + // In this test we will make sure that the validation for the script does not block saving the + // field even when no documentes where returned from the search query. + // successfully even though the script is invalid. + const error = createPreviewError({ reason: 'Houston we got a problem' }); + httpRequestsMockHelpers.setFieldPreviewResponse({ values: [], error, status: 400 }); + setSearchResponse([]); + + const onSave: jest.Mock = jest.fn(); + + const { + find, + actions: { toggleFormRow, fields, waitForUpdates }, + } = await setup({ onSave }); + + await fields.updateName('someName'); + await toggleFormRow('value'); + await fields.updateScript('echo("hello")'); + + await waitForUpdates(); // Wait for validation... it should not block and wait for preview response + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + expect(onSave).toBeCalled(); + const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'keyword', script: { source: 'echo("hello")' }, }); }); 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 index 068ebce638aa1..305cf84d59622 100644 --- 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 @@ -21,12 +21,12 @@ import { spyIndexPatternGetAllFields, spySearchQuery, spySearchQueryResponse, + TestDoc, } from './helpers'; const defaultProps: Props = { onSave: () => {}, onCancel: () => {}, - runtimeFieldValidator: () => Promise.resolve(null), isSavingField: false, }; @@ -38,12 +38,6 @@ export const setIndexPatternFields = (fields: Array<{ name: string; displayName: 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; 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 index 67309aab44a76..2403ae8c12e51 100644 --- 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 @@ -7,22 +7,21 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment, fieldFormatsOptions, indexPatternNameForTest } from './helpers'; +import { + setupEnvironment, + fieldFormatsOptions, + indexPatternNameForTest, + EsDoc, + setSearchResponseLatency, +} 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; -} +import { mockDocuments, createPreviewError } from './helpers/mocks'; describe('Field editor Preview panel', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -38,36 +37,6 @@ describe('Field editor Preview panel', () => { 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 }> = [ @@ -86,43 +55,31 @@ describe('Field editor Preview panel', () => { ]; beforeEach(async () => { + server.respondImmediately = true; + server.autoRespond = true; + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] }); setIndexPatternFields(indexPatternFields); setSearchResponse(mockDocuments); + setSearchResponseLatency(0); 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); + test('should display the preview panel along with the editor', async () => { + const { exists } = testBed; - 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 }, + actions: { toggleFormRow, fields }, } = 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}`); @@ -130,12 +87,11 @@ describe('Field editor Preview panel', () => { test('should list the list of fields of the index pattern', async () => { const { - actions: { toggleFormRow, fields, getRenderedIndexPatternFields, waitForUpdates }, + actions: { toggleFormRow, fields, getRenderedIndexPatternFields }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForUpdates(); expect(getRenderedIndexPatternFields()).toEqual([ { @@ -158,18 +114,11 @@ describe('Field editor Preview panel', () => { exists, find, component, - actions: { - toggleFormRow, - fields, - setFilterFieldsValue, - getRenderedIndexPatternFields, - waitForUpdates, - }, + actions: { toggleFormRow, fields, setFilterFieldsValue, getRenderedIndexPatternFields }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForUpdates(); // Should find a single field await setFilterFieldsValue('descr'); @@ -218,26 +167,21 @@ describe('Field editor Preview panel', () => { 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); + expect(fieldsRendered).not.toBe(null); + 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'); + expect(fieldsRendered!.at(2).text()).toBe('descriptionFirst doc - description'); // Click the third field in the list ("description") - const descriptionField = fieldsRendered.at(2); + const descriptionField = fieldsRendered!.at(2); find('pinFieldButton', descriptionField).simulate('click'); component.update(); @@ -252,7 +196,7 @@ describe('Field editor Preview panel', () => { test('should display an empty prompt if no name and no script are defined', async () => { const { exists, - actions: { toggleFormRow, fields, waitForUpdates }, + actions: { toggleFormRow, fields }, } = testBed; await toggleFormRow('value'); @@ -260,20 +204,16 @@ describe('Field editor Preview panel', () => { 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); }); @@ -286,9 +226,8 @@ describe('Field editor Preview panel', () => { }, }; - // 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. + // We open the editor with a field to edit 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 }); }); @@ -296,7 +235,6 @@ describe('Field editor Preview panel', () => { const { exists, component } = testBed; component.update(); - expect(exists('previewPanel')).toBe(true); expect(exists('previewPanel.emptyPrompt')).toBe(false); }); @@ -310,9 +248,6 @@ describe('Field editor Preview panel', () => { }, }; - // 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 }); }); @@ -320,7 +255,6 @@ describe('Field editor Preview panel', () => { const { exists, component } = testBed; component.update(); - expect(exists('previewPanel')).toBe(true); expect(exists('previewPanel.emptyPrompt')).toBe(false); }); }); @@ -328,14 +262,15 @@ describe('Field editor Preview panel', () => { describe('key & value', () => { test('should set an empty value when no script is provided', async () => { const { - actions: { toggleFormRow, fields, getRenderedFieldsPreview, waitForUpdates }, + actions: { toggleFormRow, fields, getRenderedFieldsPreview }, } = testBed; await toggleFormRow('value'); await fields.updateName('myRuntimeField'); - await waitForUpdates(); - expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: '-' }]); + expect(getRenderedFieldsPreview()).toEqual([ + { key: 'myRuntimeField', value: 'Value not set' }, + ]); }); test('should set the value returned by the painless _execute API', async () => { @@ -346,7 +281,7 @@ describe('Field editor Preview panel', () => { actions: { toggleFormRow, fields, - waitForDocumentsAndPreviewUpdate, + waitForUpdates, getLatestPreviewHttpRequest, getRenderedFieldsPreview, }, @@ -355,7 +290,7 @@ describe('Field editor Preview panel', () => { await toggleFormRow('value'); await fields.updateName('myRuntimeField'); await fields.updateScript('echo("hello")'); - await waitForDocumentsAndPreviewUpdate(); + await waitForUpdates(); // Run validations const request = getLatestPreviewHttpRequest(server); // Make sure the payload sent is correct @@ -379,46 +314,6 @@ describe('Field editor Preview panel', () => { ]); }); - 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 { @@ -445,12 +340,12 @@ describe('Field editor Preview panel', () => { const { actions: { toggleFormRow, fields, waitForUpdates, getRenderedFieldsPreview }, } = testBed; + await waitForUpdates(); // fetch documents 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 + await waitForUpdates(); // Run validations // We render the value from the _execute API expect(getRenderedFieldsPreview()).toEqual([ @@ -468,6 +363,71 @@ describe('Field editor Preview panel', () => { }); }); + describe('updating indicator', () => { + beforeEach(async () => { + // Add some latency to be able to test the "updatingIndicator" state + setSearchResponseLatency(2000); + testBed = await setup(); + }); + + test('should display an updating indicator while fetching the docs and the preview', async () => { + // We want to test if the loading indicator is in the DOM, for that we don't want the server to + // respond immediately. We'll manualy send the response. + server.respondImmediately = false; + server.autoRespond = false; + + httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['ok'] }); + + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates }, + } = testBed; + await fields.updateName('myRuntimeField'); // Give a name to remove the empty prompt + expect(exists('isUpdatingIndicator')).toBe(true); // indicator while fetching the docs + + await waitForUpdates(); // wait for docs to be fetched + expect(exists('isUpdatingIndicator')).toBe(false); + + await toggleFormRow('value'); + expect(exists('isUpdatingIndicator')).toBe(false); + + await fields.updateScript('echo("hello")'); + expect(exists('isUpdatingIndicator')).toBe(true); // indicator while getting preview + + server.respond(); + await waitForUpdates(); + 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'] }); + // We want to test if the loading indicator is in the DOM, for that we need to manually + // send the response from the server + server.respondImmediately = false; + server.autoRespond = false; + + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, + } = testBed; + await waitForUpdates(); // wait for docs to be fetched + + await toggleFormRow('value'); + await fields.updateName('myRuntimeField'); + await fields.updateScript('echo("hello")'); + expect(exists('isUpdatingIndicator')).toBe(true); + + server.respond(); + 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('format', () => { test('should apply the format to the value', async () => { /** @@ -513,32 +473,25 @@ describe('Field editor Preview panel', () => { const { exists, - find, - actions: { - toggleFormRow, - fields, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, - getRenderedFieldsPreview, - }, + actions: { toggleFormRow, fields, waitForUpdates, getRenderedFieldsPreview }, } = testBed; + expect(exists('scriptErrorBadge')).toBe(false); + await fields.updateName('myRuntimeField'); await toggleFormRow('value'); await fields.updateScript('bad()'); - await waitForDocumentsAndPreviewUpdate(); + await waitForUpdates(); // Run validations - 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); + expect(exists('scriptErrorBadge')).toBe(true); + expect(fields.getScriptError()).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(exists('scriptErrorBadge')).toBe(false); + expect(fields.getScriptError()).toBe(null); expect(getRenderedFieldsPreview()).toEqual([{ key: 'myRuntimeField', value: 'ok' }]); }); @@ -547,12 +500,12 @@ describe('Field editor Preview panel', () => { exists, find, form, - actions: { toggleFormRow, fields, waitForUpdates, waitForDocumentsAndPreviewUpdate }, + component, + actions: { toggleFormRow, fields }, } = testBed; await fields.updateName('myRuntimeField'); await toggleFormRow('value'); - await waitForDocumentsAndPreviewUpdate(); // We will return no document from the search setSearchResponse([]); @@ -560,12 +513,34 @@ describe('Field editor Preview panel', () => { await act(async () => { form.setInputValue('documentIdField', 'wrongID'); }); - await waitForUpdates(); + component.update(); - expect(exists('previewError')).toBe(true); - expect(find('previewError').text()).toContain('Document ID not found'); + expect(exists('fetchDocError')).toBe(true); + expect(find('fetchDocError').text()).toContain('Document ID not found'); expect(exists('isUpdatingIndicator')).toBe(false); }); + + test('should clear the error when disabling "Set value"', async () => { + const error = createPreviewError({ reason: 'Houston we got a problem' }); + httpRequestsMockHelpers.setFieldPreviewResponse({ values: [], error, status: 400 }); + + const { + exists, + actions: { toggleFormRow, fields, waitForUpdates }, + } = testBed; + + await toggleFormRow('value'); + await fields.updateScript('bad()'); + await waitForUpdates(); // Run validations + + expect(exists('scriptErrorBadge')).toBe(true); + expect(fields.getScriptError()).toBe(error.caused_by.reason); + + await toggleFormRow('value', 'off'); + + expect(exists('scriptErrorBadge')).toBe(false); + expect(fields.getScriptError()).toBe(null); + }); }); describe('Cluster document load and navigation', () => { @@ -581,19 +556,10 @@ describe('Field editor Preview panel', () => { test('should update the field list when the document changes', async () => { const { - actions: { - toggleFormRow, - fields, - getRenderedIndexPatternFields, - goToNextDocument, - goToPreviousDocument, - waitForUpdates, - }, + actions: { fields, getRenderedIndexPatternFields, goToNextDocument, goToPreviousDocument }, } = testBed; - await toggleFormRow('value'); - await fields.updateName('myRuntimeField'); - await waitForUpdates(); + await fields.updateName('myRuntimeField'); // Give a name to remove empty prompt expect(getRenderedIndexPatternFields()[0]).toEqual({ key: 'title', @@ -636,26 +602,17 @@ describe('Field editor Preview panel', () => { test('should update the field preview value when the document changes', async () => { httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['valueDoc1'] }); const { - actions: { - toggleFormRow, - fields, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, - getRenderedFieldsPreview, - goToNextDocument, - }, + actions: { toggleFormRow, fields, 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' }]); }); @@ -665,20 +622,12 @@ describe('Field editor Preview panel', () => { component, form, exists, - actions: { - toggleFormRow, - fields, - getRenderedIndexPatternFields, - getRenderedFieldsPreview, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, - }, + actions: { toggleFormRow, fields, getRenderedIndexPatternFields, getRenderedFieldsPreview }, } = 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. @@ -697,10 +646,6 @@ describe('Field editor Preview panel', () => { form.setInputValue('documentIdField', '123456'); }); component.update(); - // We immediately remove the index pattern fields - expect(getRenderedIndexPatternFields()).toEqual([]); - - await waitForDocumentsAndPreviewUpdate(); expect(getRenderedIndexPatternFields()).toEqual([ { @@ -717,8 +662,6 @@ describe('Field editor Preview panel', () => { }, ]); - await waitForUpdates(); // Then wait for the preview HTTP request - // The preview should have updated expect(getRenderedFieldsPreview()).toEqual([ { key: 'myRuntimeField', value: 'loadedDocPreview' }, @@ -735,18 +678,10 @@ describe('Field editor Preview panel', () => { form, component, find, - actions: { - toggleFormRow, - fields, - getRenderedFieldsPreview, - getRenderedIndexPatternFields, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, - }, + actions: { toggleFormRow, fields, getRenderedFieldsPreview, waitForUpdates }, } = testBed; await toggleFormRow('value'); - await waitForUpdates(); // fetch documents await fields.updateName('myRuntimeField'); await fields.updateScript('echo("hello world")'); await waitForUpdates(); // fetch preview @@ -758,7 +693,7 @@ describe('Field editor Preview panel', () => { await act(async () => { form.setInputValue('documentIdField', '123456'); }); - await waitForDocumentsAndPreviewUpdate(); + component.update(); // Load back the cluster data httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['clusterDataDocPreview'] }); @@ -768,10 +703,6 @@ describe('Field editor Preview panel', () => { 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([ @@ -779,22 +710,16 @@ describe('Field editor Preview panel', () => { ]); }); - test('should not lose the state of single document vs cluster data after displaying the empty prompt', async () => { + test('should not lose the state of single document vs cluster data after toggling on/off the empty prompt', async () => { const { form, component, exists, - actions: { - toggleFormRow, - fields, - getRenderedIndexPatternFields, - waitForDocumentsAndPreviewUpdate, - }, + actions: { toggleFormRow, fields, getRenderedIndexPatternFields }, } = 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); @@ -806,7 +731,6 @@ describe('Field editor Preview panel', () => { form.setInputValue('documentIdField', '123456'); }); component.update(); - await waitForDocumentsAndPreviewUpdate(); expect(exists('documentsNav')).toBe(false); expect(exists('loadDocsFromClusterButton')).toBe(true); @@ -833,24 +757,20 @@ describe('Field editor Preview panel', () => { form, component, find, - actions: { toggleFormRow, fields, waitForUpdates }, + actions: { fields }, } = testBed; const expectedParamsToFetchClusterData = { - params: { index: 'testIndexPattern', body: { size: 50 } }, + params: { index: indexPatternNameForTest, 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(); + await fields.updateName('myRuntimeField'); // hide the empty prompt searchMeta = getSearchCallMeta(); - expect(searchMeta.totalCalls).toBe(initialCount + 1); + const initialCount = searchMeta.totalCalls; expect(searchMeta.lastCallParams).toEqual(expectedParamsToFetchClusterData); // Load single doc @@ -860,10 +780,9 @@ describe('Field editor Preview panel', () => { form.setInputValue('documentIdField', nextId); }); component.update(); - await waitForUpdates(); searchMeta = getSearchCallMeta(); - expect(searchMeta.totalCalls).toBe(initialCount + 2); + expect(searchMeta.totalCalls).toBe(initialCount + 1); expect(searchMeta.lastCallParams).toEqual({ params: { body: { @@ -874,7 +793,7 @@ describe('Field editor Preview panel', () => { }, size: 1, }, - index: 'testIndexPattern', + index: indexPatternNameForTest, }, }); @@ -884,8 +803,30 @@ describe('Field editor Preview panel', () => { find('loadDocsFromClusterButton').simulate('click'); }); searchMeta = getSearchCallMeta(); - expect(searchMeta.totalCalls).toBe(initialCount + 3); + expect(searchMeta.totalCalls).toBe(initialCount + 2); expect(searchMeta.lastCallParams).toEqual(expectedParamsToFetchClusterData); }); }); + + describe('When no documents could be fetched from cluster', () => { + beforeEach(() => { + setSearchResponse([]); + }); + + test('should not display the updating indicator and have a callout to indicate that preview is not available', async () => { + setSearchResponseLatency(2000); + testBed = await setup(); + + const { + exists, + actions: { fields, waitForUpdates }, + } = testBed; + await fields.updateName('myRuntimeField'); // Give a name to remove the empty prompt + expect(exists('isUpdatingIndicator')).toBe(true); // indicator while fetching the docs + + await waitForUpdates(); // wait for docs to be fetched + expect(exists('isUpdatingIndicator')).toBe(false); + expect(exists('previewNotAvailableCallout')).toBe(true); + }); + }); }); 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 index ca061968dae20..9f8b52af5878e 100644 --- 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 @@ -8,6 +8,36 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; +/** + * We often need to wait for both the documents & the preview to be fetched. + * We can't increase the `jest.advanceTimersByTime()` time + * as those are 2 different operations that occur in sequence. + */ +export const waitForDocumentsAndPreviewUpdate = async (testBed?: TestBed) => { + // Wait for documents to be fetched + await act(async () => { + jest.advanceTimersByTime(5000); + }); + + // Wait for the syntax validation debounced + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + testBed?.component.update(); +}; + +/** + * Handler to bypass the debounce time in our tests + */ +export const waitForUpdates = async (testBed?: TestBed) => { + await act(async () => { + jest.advanceTimersByTime(5000); + }); + + testBed?.component.update(); +}; + export const getCommonActions = (testBed: TestBed) => { const toggleFormRow = async ( row: 'customLabel' | 'value' | 'format', @@ -66,46 +96,28 @@ export const getCommonActions = (testBed: TestBed) => { 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(); - }); + const getScriptError = () => { + const scriptError = testBed.component.find('#runtimeFieldScript-error-0'); - 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(); - }); + if (scriptError.length === 0) { + return null; + } else if (scriptError.length > 1) { + return scriptError.at(0).text(); + } - testBed.component.update(); + return scriptError.text(); }; return { toggleFormRow, - waitForUpdates, - waitForDocumentsAndPreviewUpdate, + waitForUpdates: waitForUpdates.bind(null, testBed), + waitForDocumentsAndPreviewUpdate: waitForDocumentsAndPreviewUpdate.bind(null, testBed), fields: { updateName, updateType, updateScript, updateFormat, + getScriptError, }, }; }; 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 index e8ff7eb7538f2..2fc870bd42d66 100644 --- 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 @@ -17,6 +17,14 @@ export { spyIndexPatternGetAllFields, fieldFormatsOptions, indexPatternNameForTest, + setSearchResponseLatency, } from './setup_environment'; -export { getCommonActions } from './common_actions'; +export { + getCommonActions, + waitForUpdates, + waitForDocumentsAndPreviewUpdate, +} from './common_actions'; + +export type { EsDoc, TestDoc } from './mocks'; +export { mockDocuments } from './mocks'; diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx index d33a0d2a87fb5..7161776c21fb1 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx @@ -5,7 +5,11 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React from 'react'; +import React, { useEffect } from 'react'; +import { of } from 'rxjs'; + +const mockUseEffect = useEffect; +const mockOf = of; const EDITOR_ID = 'testEditor'; @@ -39,6 +43,7 @@ jest.mock('@elastic/eui', () => { jest.mock('@kbn/monaco', () => { const original = jest.requireActual('@kbn/monaco'); + const originalMonaco = original.monaco; return { ...original, @@ -48,10 +53,28 @@ jest.mock('@kbn/monaco', () => { getSyntaxErrors: () => ({ [EDITOR_ID]: [], }), + validation$() { + return mockOf({ isValid: true, isValidating: false, errors: [] }); + }, + }, + monaco: { + ...originalMonaco, + editor: { + ...originalMonaco.editor, + setModelMarkers() {}, + }, }, }; }); +jest.mock('react-use/lib/useDebounce', () => { + return (cb: () => void, ms: number, deps: any[]) => { + mockUseEffect(() => { + cb(); + }, deps); + }; +}); + jest.mock('../../../../kibana_react/public', () => { const original = jest.requireActual('../../../../kibana_react/public'); @@ -60,15 +83,19 @@ jest.mock('../../../../kibana_react/public', () => { * 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, - }; - }, - }); + const { editorDidMount } = props; + + mockUseEffect(() => { + // Forward our deterministic ID to the consumer + // We need below for the PainlessLang.getSyntaxErrors mock + editorDidMount({ + getModel() { + return { + id: EDITOR_ID, + }; + }, + }); + }, [editorDidMount]); return ( Promise.resolve({})); export const spyIndexPatternGetAllFields = jest.fn().mockImplementation(() => []); -spySearchQuery.mockImplementation((params) => { +let searchResponseDelay = 0; + +// Add latency to the search request +export const setSearchResponseLatency = (ms: number) => { + searchResponseDelay = ms; +}; + +spySearchQuery.mockImplementation(() => { return { toPromise: () => { + if (searchResponseDelay === 0) { + // no delay, it is synchronous + return spySearchQueryResponse(); + } + return new Promise((resolve) => { setTimeout(() => { resolve(undefined); - }, 2000); // simulate 2s latency for the HTTP request - }).then(() => spySearchQueryResponse()); + }, searchResponseDelay); + }).then(() => { + return spySearchQueryResponse(); + }); }, }; }); 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 11183d575e955..ddc3aa72c7610 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 @@ -42,7 +42,6 @@ import { ScriptField, FormatField, PopularityField, - ScriptSyntaxError, } from './form_fields'; import { FormRow } from './form_row'; import { AdvancedParametersSection } from './advanced_parameters_section'; @@ -50,6 +49,7 @@ import { AdvancedParametersSection } from './advanced_parameters_section'; export interface FieldEditorFormState { isValid: boolean | undefined; isSubmitted: boolean; + isSubmitting: boolean; submit: FormHook['submit']; } @@ -70,7 +70,6 @@ export interface Props { onChange?: (state: FieldEditorFormState) => void; /** Handler to receive update on the form "isModified" state */ onFormModifiedChange?: (isModified: boolean) => void; - syntaxError: ScriptSyntaxError; } const geti18nTexts = (): { @@ -150,12 +149,11 @@ const formSerializer = (field: FieldFormInternal): Field => { }; }; -const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxError }: Props) => { +const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) => { const { links, namesNotAllowed, existingConcreteFields, fieldTypeToProcess } = useFieldEditorContext(); const { params: { update: updatePreviewParams }, - panel: { setIsVisible: setIsPanelVisible }, } = useFieldPreviewContext(); const { form } = useForm({ defaultValue: field, @@ -163,8 +161,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr deserializer: formDeserializer, serializer: formSerializer, }); - const { submit, isValid: isFormValid, isSubmitted, getFields } = form; - const { clear: clearSyntaxError } = syntaxError; + const { submit, isValid: isFormValid, isSubmitted, getFields, isSubmitting } = form; const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); const i18nTexts = geti18nTexts(); @@ -191,19 +188,12 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr 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 }); + onChange({ isValid: isFormValid, isSubmitted, isSubmitting, submit }); } - }, [onChange, isFormValid, isSubmitted, submit]); - - useEffect(() => { - // Whenever the field "type" changes we clear any possible painless syntax - // error as it is possibly stale. - clearSyntaxError(); - }, [updatedType, clearSyntaxError]); + }, [onChange, isFormValid, isSubmitted, isSubmitting, submit]); useEffect(() => { updatePreviewParams({ @@ -217,14 +207,6 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr }); }, [updatedName, updatedType, updatedScript, isValueVisible, updatedFormat, updatePreviewParams]); - useEffect(() => { - if (isValueVisible || isFormatVisible) { - setIsPanelVisible(true); - } else { - setIsPanelVisible(false); - } - }, [isValueVisible, isFormatVisible, setIsPanelVisible]); - useEffect(() => { if (onFormModifiedChange) { onFormModifiedChange(isFormModified); @@ -236,6 +218,8 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr form={form} className="indexPatternFieldEditor__form" data-test-subj="indexPatternFieldEditorForm" + isInvalid={isSubmitted && isFormValid === false} + error={form.getErrors()} > {/* Name */} @@ -296,11 +280,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, syntaxErr data-test-subj="valueRow" withDividerRule > - + )} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts index 693709729ed92..cfa09db3cdc83 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts @@ -12,7 +12,6 @@ export { CustomLabelField } from './custom_label_field'; export { PopularityField } from './popularity_field'; -export type { ScriptSyntaxError } from './script_field'; export { ScriptField } from './script_field'; export { FormatField } from './format_field'; 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 d73e8046e5db7..b1dcddd459c8a 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 @@ -6,32 +6,32 @@ * Side Public License, v 1. */ -import React, { useState, useEffect, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; +import { first } from 'rxjs/operators'; +import type { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiLink, EuiCode, EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { PainlessLang, PainlessContext } from '@kbn/monaco'; +import { EuiFormRow, EuiLink, EuiCode } from '@elastic/eui'; +import { PainlessLang, PainlessContext, monaco } from '@kbn/monaco'; +import { firstValueFrom } from '@kbn/std'; import { UseField, useFormData, + useBehaviorSubject, RuntimeType, - FieldConfig, CodeEditor, + useFormContext, } from '../../../shared_imports'; -import { RuntimeFieldPainlessError } from '../../../lib'; +import type { RuntimeFieldPainlessError } from '../../../types'; +import { painlessErrorToMonacoMarker } from '../../../lib'; +import { useFieldPreviewContext, Context } from '../../preview'; import { schema } from '../form_schema'; import type { FieldFormInternal } from '../field_editor'; interface Props { links: { runtimePainless: string }; existingConcreteFields?: Array<{ name: string; type: string }>; - syntaxError: ScriptSyntaxError; -} - -export interface ScriptSyntaxError { - error: RuntimeFieldPainlessError | null; - clear: () => void; } const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessContext => { @@ -53,87 +53,166 @@ const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessConte } }; -export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxError }: Props) => { - const editorValidationTimeout = useRef>(); +const ScriptFieldComponent = ({ existingConcreteFields, links }: Props) => { + const monacoEditor = useRef(null); + const editorValidationSubscription = useRef(); + const fieldCurrentValue = useRef(''); + + const { + error, + isLoadingPreview, + isPreviewAvailable, + currentDocument: { isLoading: isFetchingDoc, value: currentDocument }, + validation: { setScriptEditorValidation }, + } = useFieldPreviewContext(); + const [validationData$, nextValidationData$] = useBehaviorSubject< + | { + isFetchingDoc: boolean; + isLoadingPreview: boolean; + error: Context['error']; + } + | undefined + >(undefined); const [painlessContext, setPainlessContext] = useState( - mapReturnTypeToPainlessContext(schema.type.defaultValue[0].value!) + mapReturnTypeToPainlessContext(schema.type.defaultValue![0].value!) + ); + + const currentDocId = currentDocument?._id; + + const suggestionProvider = useMemo( + () => PainlessLang.getSuggestionProvider(painlessContext, existingConcreteFields), + [painlessContext, existingConcreteFields] ); - const [editorId, setEditorId] = useState(); + const { validateFields } = useFormContext(); - const suggestionProvider = PainlessLang.getSuggestionProvider( - painlessContext, - existingConcreteFields + // Listen to formData changes **before** validations are executed + const onFormDataChange = useCallback( + ({ type }: FieldFormInternal) => { + if (type !== undefined) { + setPainlessContext(mapReturnTypeToPainlessContext(type[0]!.value!)); + } + + if (isPreviewAvailable) { + // To avoid a race condition where the validation would run before + // the context state are updated, we clear the old value of the observable. + // This way the validationDataProvider() will await until new values come in before resolving + nextValidationData$(undefined); + } + }, + [nextValidationData$, isPreviewAvailable] ); - const [{ type, script: { source } = { source: '' } }] = useFormData({ + useFormData({ watch: ['type', 'script.source'], + onChange: onFormDataChange, }); - const { clear: clearSyntaxError } = syntaxError; - - const sourceFieldConfig: FieldConfig = useMemo(() => { - return { - ...schema.script.source, - validations: [ - ...schema.script.source.validations, - { - validator: () => { - if (editorValidationTimeout.current) { - clearTimeout(editorValidationTimeout.current); - } - - return new Promise((resolve) => { - // monaco waits 500ms before validating, so we also add a delay - // before checking if there are any syntax errors - editorValidationTimeout.current = setTimeout(() => { - const painlessSyntaxErrors = PainlessLang.getSyntaxErrors(); - // It is possible for there to be more than one editor in a view, - // so we need to get the syntax errors based on the editor (aka model) ID - const editorHasSyntaxErrors = - editorId && - painlessSyntaxErrors[editorId] && - painlessSyntaxErrors[editorId].length > 0; - - if (editorHasSyntaxErrors) { - return resolve({ - message: i18n.translate( - 'indexPatternFieldEditor.editor.form.scriptEditorValidationMessage', - { - defaultMessage: 'Invalid Painless syntax.', - } - ), - }); - } - - resolve(undefined); - }, 600); - }); - }, - }, - ], - }; - }, [editorId]); + const validationDataProvider = useCallback(async () => { + const validationData = await firstValueFrom( + validationData$.pipe( + first((data) => { + // We first wait to get field preview data + if (data === undefined) { + return false; + } + + // We are not interested in preview data meanwhile it + // is still making HTTP request + if (data.isFetchingDoc || data.isLoadingPreview) { + return false; + } + + return true; + }) + ) + ); + + return validationData!.error; + }, [validationData$]); + + const onEditorDidMount = useCallback( + (editor: monaco.editor.IStandaloneCodeEditor) => { + monacoEditor.current = editor; + + if (editorValidationSubscription.current) { + editorValidationSubscription.current.unsubscribe(); + } + + editorValidationSubscription.current = PainlessLang.validation$().subscribe( + ({ isValid, isValidating, errors }) => { + setScriptEditorValidation({ + isValid, + isValidating, + message: errors[0]?.message ?? null, + }); + } + ); + }, + [setScriptEditorValidation] + ); + + const updateMonacoMarkers = useCallback((markers: monaco.editor.IMarkerData[]) => { + const model = monacoEditor.current?.getModel(); + if (model) { + monaco.editor.setModelMarkers(model, PainlessLang.ID, markers); + } + }, []); + + const displayPainlessScriptErrorInMonaco = useCallback( + (painlessError: RuntimeFieldPainlessError) => { + const model = monacoEditor.current?.getModel(); + + if (painlessError.position !== null && Boolean(model)) { + const { offset } = painlessError.position; + // Get the monaco Position (lineNumber and colNumber) from the ES Painless error position + const errorStartPosition = model!.getPositionAt(offset); + const markerData = painlessErrorToMonacoMarker(painlessError, errorStartPosition); + const errorMarkers = markerData ? [markerData] : []; + updateMonacoMarkers(errorMarkers); + } + }, + [updateMonacoMarkers] + ); + + // Whenever we navigate to a different doc we validate the script + // field as it could be invalid against the new document. + useEffect(() => { + if (fieldCurrentValue.current.trim() !== '' && currentDocId !== undefined) { + validateFields(['script.source']); + } + }, [currentDocId, validateFields]); useEffect(() => { - setPainlessContext(mapReturnTypeToPainlessContext(type[0]!.value!)); - }, [type]); + nextValidationData$({ isFetchingDoc, isLoadingPreview, error }); + }, [nextValidationData$, isFetchingDoc, isLoadingPreview, error]); useEffect(() => { - // Whenever the source changes we clear potential syntax errors - clearSyntaxError(); - }, [source, clearSyntaxError]); + if (error?.code === 'PAINLESS_SCRIPT_ERROR') { + displayPainlessScriptErrorInMonaco(error!.error as RuntimeFieldPainlessError); + } else if (error === null) { + updateMonacoMarkers([]); + } + }, [error, displayPainlessScriptErrorInMonaco, updateMonacoMarkers]); + + useEffect(() => { + return () => { + if (editorValidationSubscription.current) { + editorValidationSubscription.current.unsubscribe(); + } + }; + }, []); return ( - path="script.source" config={sourceFieldConfig}> + path="script.source" validationDataProvider={validationDataProvider}> {({ value, setValue, label, isValid, getErrorsMessages }) => { - let errorMessage: string | null = ''; - if (syntaxError.error !== null) { - errorMessage = syntaxError.error.reason ?? syntaxError.error.message; - } else { - errorMessage = getErrorsMessages(); + let errorMessage = getErrorsMessages(); + + if (error) { + errorMessage = error.error.reason!; } + fieldCurrentValue.current = value; return ( <> @@ -141,7 +220,7 @@ export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxEr label={label} id="runtimeFieldScript" error={errorMessage} - isInvalid={syntaxError.error !== null || !isValid} + isInvalid={!isValid} helpText={ setEditorId(editor.getModel()?.id)} + editorDidMount={onEditorDidMount} options={{ fontSize: 12, minimap: { @@ -199,33 +278,11 @@ export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxEr )} /> - - {/* Help the user debug the error by showing where it failed in the script */} - {syntaxError.error !== null && ( - <> - - -

    - {i18n.translate( - 'indexPatternFieldEditor.editor.form.scriptEditor.debugErrorMessage', - { - defaultMessage: 'Syntax error detail', - } - )} -

    -
    - - - {syntaxError.error.scriptStack.join('\n')} - - - )} ); }}
    ); -}); +}; + +export const ScriptField = React.memo(ScriptFieldComponent); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts index 979a1fdb1adc1..7a15dce3af019 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts @@ -7,11 +7,77 @@ */ import { i18n } from '@kbn/i18n'; -import { fieldValidators } from '../../shared_imports'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { Subscription } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { PainlessLang } from '@kbn/monaco'; +import { + fieldValidators, + FieldConfig, + RuntimeType, + ValidationFunc, + ValidationCancelablePromise, +} from '../../shared_imports'; +import type { Context } from '../preview'; import { RUNTIME_FIELD_OPTIONS } from './constants'; const { containsCharsField, emptyField, numberGreaterThanField } = fieldValidators; +const i18nTexts = { + invalidScriptErrorMessage: i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditorPainlessValidationMessage', + { + defaultMessage: 'Invalid Painless script.', + } + ), +}; + +// Validate the painless **syntax** (no need to make an HTTP request) +const painlessSyntaxValidator = () => { + let isValidatingSub: Subscription; + + return (() => { + const promise: ValidationCancelablePromise<'ERR_PAINLESS_SYNTAX'> = new Promise((resolve) => { + isValidatingSub = PainlessLang.validation$() + .pipe( + first(({ isValidating }) => { + return isValidating === false; + }) + ) + .subscribe(({ errors }) => { + const editorHasSyntaxErrors = errors.length > 0; + + if (editorHasSyntaxErrors) { + return resolve({ + message: i18nTexts.invalidScriptErrorMessage, + code: 'ERR_PAINLESS_SYNTAX', + }); + } + + resolve(undefined); + }); + }); + + promise.cancel = () => { + if (isValidatingSub) { + isValidatingSub.unsubscribe(); + } + }; + + return promise; + }) as ValidationFunc; +}; + +// Validate the painless **script** +const painlessScriptValidator: ValidationFunc = async ({ customData: { provider } }) => { + const previewError = (await provider()) as Context['error']; + + if (previewError && previewError.code === 'PAINLESS_SCRIPT_ERROR') { + return { + message: i18nTexts.invalidScriptErrorMessage, + }; + } +}; export const schema = { name: { @@ -47,7 +113,8 @@ export const schema = { defaultMessage: 'Type', }), defaultValue: [RUNTIME_FIELD_OPTIONS[0]], - }, + fieldsToValidateOnChange: ['script.source'], + } as FieldConfig>>, script: { source: { label: i18n.translate('indexPatternFieldEditor.editor.form.defineFieldLabel', { @@ -64,6 +131,14 @@ export const schema = { ) ), }, + { + validator: painlessSyntaxValidator(), + isAsync: true, + }, + { + validator: painlessScriptValidator, + isAsync: true, + }, ], }, }, 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 f13b30f13327c..d1dbb50ebf2e4 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,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -15,13 +15,10 @@ import { EuiFlexItem, EuiButtonEmpty, EuiButton, - EuiCallOut, - EuiSpacer, EuiText, } from '@elastic/eui'; -import type { Field, EsRuntimeField } from '../types'; -import { RuntimeFieldPainlessError } from '../lib'; +import type { Field } from '../types'; import { euiFlyoutClassname } from '../constants'; import { FlyoutPanels } from './flyout_panels'; import { useFieldEditorContext } from './field_editor_context'; @@ -36,9 +33,6 @@ const i18nTexts = { saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { defaultMessage: 'Save', }), - formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', { - defaultMessage: 'Fix errors in form before continuing.', - }), }; const defaultModalVisibility = { @@ -55,8 +49,6 @@ export interface Props { * Handler for the "cancel" footer button */ onCancel: () => void; - /** Handler to validate the script */ - runtimeFieldValidator: (field: EsRuntimeField) => Promise; /** Optional field to process */ field?: Field; isSavingField: boolean; @@ -70,10 +62,10 @@ const FieldEditorFlyoutContentComponent = ({ field, onSave, onCancel, - runtimeFieldValidator, isSavingField, onMounted, }: Props) => { + const isMounted = useRef(false); const isEditingExistingField = !!field; const { indexPattern } = useFieldEditorContext(); const { @@ -82,32 +74,18 @@ const FieldEditorFlyoutContentComponent = ({ const [formState, setFormState] = useState({ isSubmitted: false, + isSubmitting: false, isValid: field ? true : undefined, submit: field ? async () => ({ isValid: true, data: field }) : async () => ({ isValid: false, data: {} as Field }), }); - const [painlessSyntaxError, setPainlessSyntaxError] = useState( - null - ); - - const [isValidating, setIsValidating] = useState(false); const [modalVisibility, setModalVisibility] = useState(defaultModalVisibility); const [isFormModified, setIsFormModified] = useState(false); - const { submit, isValid: isFormValid, isSubmitted } = formState; - const hasErrors = isFormValid === false || painlessSyntaxError !== null; - - const clearSyntaxError = useCallback(() => setPainlessSyntaxError(null), []); - - const syntaxError = useMemo( - () => ({ - error: painlessSyntaxError, - clear: clearSyntaxError, - }), - [painlessSyntaxError, clearSyntaxError] - ); + const { submit, isValid: isFormValid, isSubmitting } = formState; + const hasErrors = isFormValid === false; const canCloseValidator = useCallback(() => { if (isFormModified) { @@ -121,25 +99,15 @@ const FieldEditorFlyoutContentComponent = ({ const onClickSave = useCallback(async () => { const { isValid, data } = await submit(); - const nameChange = field?.name !== data.name; - const typeChange = field?.type !== data.type; - - if (isValid) { - if (data.script) { - setIsValidating(true); - - const error = await runtimeFieldValidator({ - type: data.type, - script: data.script, - }); - setIsValidating(false); - setPainlessSyntaxError(error); + if (!isMounted.current) { + // User has closed the flyout meanwhile submitting the form + return; + } - if (error) { - return; - } - } + if (isValid) { + const nameChange = field?.name !== data.name; + const typeChange = field?.type !== data.type; if (isEditingExistingField && (nameChange || typeChange)) { setModalVisibility({ @@ -150,7 +118,7 @@ const FieldEditorFlyoutContentComponent = ({ onSave(data); } } - }, [onSave, submit, runtimeFieldValidator, field, isEditingExistingField]); + }, [onSave, submit, field, isEditingExistingField]); const onClickCancel = useCallback(() => { const canClose = canCloseValidator(); @@ -206,6 +174,14 @@ const FieldEditorFlyoutContentComponent = ({ } }, [onMounted, canCloseValidator]); + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + return ( <> <> - {isSubmitted && hasErrors && ( - <> - - - - )} {i18nTexts.saveButtonLabel} 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 ea981662c1ff7..1738c55ba1f55 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 @@ -20,7 +20,7 @@ import { } from '../shared_imports'; import type { Field, PluginStart, InternalFieldType } from '../types'; import { pluginName } from '../constants'; -import { deserializeField, getRuntimeFieldValidator, getLinks, ApiService } from '../lib'; +import { deserializeField, getLinks, ApiService } from '../lib'; import { FieldEditorFlyoutContent, Props as FieldEditorFlyoutContentProps, @@ -103,11 +103,6 @@ export const FieldEditorFlyoutContentContainer = ({ return existing; }, [fields, field]); - const validateRuntimeField = useMemo( - () => getRuntimeFieldValidator(indexPattern.title, search), - [search, indexPattern] - ); - const services = useMemo( () => ({ api: apiService, @@ -207,7 +202,6 @@ export const FieldEditorFlyoutContentContainer = ({ onCancel={onCancel} onMounted={onMounted} field={fieldToEdit} - runtimeFieldValidator={validateRuntimeField} isSavingField={isSaving} /> 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 index fa4097725cde1..04f5e2e542f40 100644 --- 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 @@ -21,22 +21,11 @@ import { useFieldPreviewContext } from './field_preview_context'; export const DocumentsNavPreview = () => { const { currentDocument: { id: documentId, isCustomId }, - documents: { loadSingle, loadFromCluster }, + documents: { loadSingle, loadFromCluster, fetchDocError }, 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'; + const isInvalid = fetchDocError?.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" @@ -58,13 +47,12 @@ export const DocumentsNavPreview = () => { label={i18n.translate('indexPatternFieldEditor.fieldPreview.documentIdField.label', { defaultMessage: 'Document ID', })} - error={errorMessage} isInvalid={isInvalid} fullWidth > void; - highlighted?: boolean; + hasScriptError?: boolean; + /** Indicates whether the field list item comes from the Painless script */ + isFromScript?: boolean; } export const PreviewListItem: React.FC = ({ field: { key, value, formattedValue, isPinned = false }, - highlighted, toggleIsPinned, + hasScriptError, + isFromScript = false, }) => { + const { isLoadingPreview } = useFieldPreviewContext(); + 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--highlighted': isFromScript, 'indexPatternFieldEditor__previewFieldList__item--pinned': isPinned, }); /* eslint-enable @typescript-eslint/naming-convention */ const doesContainImage = formattedValue?.includes(' { + if (isFromScript && !Boolean(key)) { + return ( + + + {i18n.translate('indexPatternFieldEditor.fieldPreview.fieldNameNotSetLabel', { + defaultMessage: 'Field name not set', + })} + + + ); + } + + return key; + }; + + const withTooltip = (content: JSX.Element) => ( + + {content} + + ); + const renderValue = () => { + if (isFromScript && isLoadingPreview) { + return ( + + + + ); + } + + if (hasScriptError) { + return ( +
    + + {i18n.translate('indexPatternFieldEditor.fieldPreview.scriptErrorBadgeLabel', { + defaultMessage: 'Script error', + })} + +
    + ); + } + + if (isFromScript && value === undefined) { + return ( + + + {i18n.translate('indexPatternFieldEditor.fieldPreview.valueNotSetLabel', { + defaultMessage: 'Value not set', + })} + + + ); + } + if (doesContainImage) { return ( = ({ } if (formattedValue !== undefined) { - return ( + return withTooltip( = ({ ); } - return ( + return withTooltip( {JSON.stringify(value)} @@ -76,19 +145,14 @@ export const PreviewListItem: React.FC = ({ className="indexPatternFieldEditor__previewFieldList__item__key__wrapper" data-test-subj="key" > - {key} + {renderName()}
- - {renderValue()} - + {renderValue()} { }, fields, error, + documents: { fetchDocError }, reset, + isPreviewAvailable, } = useFieldPreviewContext(); // To show the preview we at least need a name to be defined, the script or the format @@ -38,12 +40,15 @@ export const FieldPreview = () => { 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 + Boolean(error) || fields.length > 0 ? false : name === null && format === null ? true : false; + const doRenderListOfFields = fetchDocError === null; + const showWarningPreviewNotAvailable = isPreviewAvailable === false && fetchDocError === null; + const onFieldListResize = useCallback(({ height }: { height: number }) => { setFieldListHeight(height); }, []); @@ -58,7 +63,7 @@ export const FieldPreview = () => { return (
  • - +
); @@ -70,9 +75,6 @@ export const FieldPreview = () => { return reset; }, [reset]); - const doShowFieldList = - error === null || (error.code !== 'DOC_NOT_FOUND' && error.code !== 'ERR_FETCHING_DOC'); - return (
{ - - - - 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} - /> -
+ {showWarningPreviewNotAvailable ? ( + +

+ {i18n.translate( + 'indexPatternFieldEditor.fieldPreview.notAvailableWarningCallout.description', + { + defaultMessage: + 'Runtime field preview is disabled because no documents could be fetched from the cluster.', + } )} - +

+
+ ) : ( + <> + + + + {doRenderListOfFields && ( + <> + setSearchValue(e.target.value)} + placeholder={i18n.translate( + 'indexPatternFieldEditor.fieldPreview.filterFieldsPlaceholder', + { + defaultMessage: 'Filter fields', + } + )} + fullWidth + data-test-subj="filterFieldsInput" + /> + + + )} + + + + + {doRenderListOfFields && ( + <> + {/* 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 index 21ab055c9b05e..74f77f91e2f13 100644 --- 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 @@ -20,81 +20,18 @@ 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>; - }; -} +import type { + PainlessExecuteContext, + Context, + Params, + ClusterData, + From, + EsDocument, + ScriptErrorCodes, + FetchDocError, +} from './types'; const fieldPreviewContext = createContext(undefined); @@ -112,7 +49,10 @@ export const defaultValueFormatter = (value: unknown) => export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const previewCount = useRef(0); - const [lastExecutePainlessRequestParams, setLastExecutePainlessReqParams] = useState<{ + + // We keep in cache the latest params sent to the _execute API so we don't make unecessary requests + // when changing parameters that don't affect the preview result (e.g. changing the "name" field). + const lastExecutePainlessRequestParams = useRef<{ type: Params['type']; script: string | undefined; documentId: string | undefined; @@ -138,6 +78,8 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { fields: Context['fields']; error: Context['error']; }>({ fields: [], error: null }); + /** Possible error while fetching sample documents */ + const [fetchDocError, setFetchDocError] = useState(null); /** The parameters required for the Painless _execute API */ const [params, setParams] = useState(defaultParams); /** The sample documents fetched from the cluster */ @@ -146,7 +88,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { currentIdx: 0, }); /** Flag to show/hide the preview panel */ - const [isPanelVisible, setIsPanelVisible] = useState(false); + const [isPanelVisible, setIsPanelVisible] = useState(true); /** 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 */ @@ -157,44 +99,66 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const [from, setFrom] = useState('cluster'); /** Map of fields pinned to the top of the list */ const [pinnedFields, setPinnedFields] = useState<{ [key: string]: boolean }>({}); + /** Keep track if the script painless syntax is being validated and if it is valid */ + const [scriptEditorValidation, setScriptEditorValidation] = useState<{ + isValidating: boolean; + isValid: boolean; + message: string | null; + }>({ isValidating: false, isValid: true, message: null }); const { documents, currentIdx } = clusterData; - const currentDocument: EsDocument | undefined = useMemo( - () => documents[currentIdx], - [documents, currentIdx] - ); - - const currentDocIndex = currentDocument?._index; - const currentDocId: string = currentDocument?._id ?? ''; + const currentDocument: EsDocument | undefined = documents[currentIdx]; + const currentDocIndex: string | undefined = currentDocument?._index; + const currentDocId: string | undefined = currentDocument?._id; const totalDocs = documents.length; + const isCustomDocId = customDocIdToLoad !== null; + let isPreviewAvailable = true; + + // If no documents could be fetched from the cluster (and we are not trying to load + // a custom doc ID) then we disable preview as the script field validation expect the result + // of the preview to before resolving. If there are no documents we can't have a preview + // (the _execute API expects one) and thus the validation should not expect any value. + if (!isFetchingDocument && !isCustomDocId && documents.length === 0) { + isPreviewAvailable = false; + } + 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) { + const allParamsDefined = useMemo(() => { + if (!currentDocIndex || !script?.source || !type) { return false; } - - const allParamsDefined = (['type', 'script', 'index', 'document'] as Array).every( - (key) => Boolean(params[key]) + return true; + }, [currentDocIndex, script?.source, type]); + + const hasSomeParamsChanged = useMemo(() => { + return ( + lastExecutePainlessRequestParams.current.type !== type || + lastExecutePainlessRequestParams.current.script !== script?.source || + lastExecutePainlessRequestParams.current.documentId !== currentDocId ); + }, [type, script, currentDocId]); - if (!allParamsDefined) { - return false; - } - - const hasSomeParamsChanged = - lastExecutePainlessRequestParams.type !== type || - lastExecutePainlessRequestParams.script !== script?.source || - lastExecutePainlessRequestParams.documentId !== currentDocId; + const setPreviewError = useCallback((error: Context['error']) => { + setPreviewResponse((prev) => ({ + ...prev, + error, + })); + }, []); - return hasSomeParamsChanged; - }, [type, script?.source, currentDocId, params, lastExecutePainlessRequestParams]); + const clearPreviewError = useCallback((errorCode: ScriptErrorCodes) => { + setPreviewResponse((prev) => { + const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error; + return { + ...prev, + error, + }; + }); + }, []); const valueFormatter = useCallback( (value: unknown) => { @@ -217,14 +181,11 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { throw new Error('The "limit" option must be a number'); } + lastExecutePainlessRequestParams.current.documentId = undefined; setIsFetchingDocument(true); - setClusterData({ - documents: [], - currentIdx: 0, - }); setPreviewResponse({ fields: [], error: null }); - const [response, error] = await search + const [response, searchError] = await search .search({ params: { index: indexPattern.title, @@ -240,12 +201,29 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { setIsFetchingDocument(false); setCustomDocIdToLoad(null); - setClusterData({ - documents: response ? response.rawResponse.hits.hits : [], - currentIdx: 0, - }); + const error: FetchDocError | null = Boolean(searchError) + ? { + code: 'ERR_FETCHING_DOC', + error: { + message: searchError.toString(), + reason: i18n.translate( + 'indexPatternFieldEditor.fieldPreview.error.errorLoadingSampleDocumentsDescription', + { + defaultMessage: 'Error loading sample documents.', + } + ), + }, + } + : null; - setPreviewResponse((prev) => ({ ...prev, error })); + setFetchDocError(error); + + if (error === null) { + setClusterData({ + documents: response ? response.rawResponse.hits.hits : [], + currentIdx: 0, + }); + } }, [indexPattern, search] ); @@ -256,6 +234,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { return; } + lastExecutePainlessRequestParams.current.documentId = undefined; setIsFetchingDocument(true); const [response, searchError] = await search @@ -280,11 +259,17 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const isDocumentFound = response?.rawResponse.hits.total > 0; const loadedDocuments: EsDocument[] = isDocumentFound ? response.rawResponse.hits.hits : []; - const error: Context['error'] = Boolean(searchError) + const error: FetchDocError | null = Boolean(searchError) ? { code: 'ERR_FETCHING_DOC', error: { message: searchError.toString(), + reason: i18n.translate( + 'indexPatternFieldEditor.fieldPreview.error.errorLoadingDocumentDescription', + { + defaultMessage: 'Error loading document.', + } + ), }, } : isDocumentFound === false @@ -301,14 +286,14 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { } : null; - setPreviewResponse((prev) => ({ ...prev, error })); + setFetchDocError(error); - setClusterData({ - documents: loadedDocuments, - currentIdx: 0, - }); - - if (error !== null) { + if (error === null) { + setClusterData({ + documents: loadedDocuments, + currentIdx: 0, + }); + } else { // Make sure we disable the "Updating..." indicator as we have an error // and we won't fetch the preview setIsLoadingPreview(false); @@ -318,23 +303,28 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { ); const updatePreview = useCallback(async () => { - setLastExecutePainlessReqParams({ - type: params.type, - script: params.script?.source, - documentId: currentDocId, - }); + if (scriptEditorValidation.isValidating) { + return; + } - if (!needToUpdatePreview) { + if (!allParamsDefined || !hasSomeParamsChanged || scriptEditorValidation.isValid === false) { + setIsLoadingPreview(false); return; } + lastExecutePainlessRequestParams.current = { + type, + script: script?.source, + documentId: currentDocId, + }; + const currentApiCall = ++previewCount.current; const response = await getFieldPreview({ - index: currentDocIndex, - document: params.document!, - context: `${params.type!}_field` as FieldPreviewContext, - script: params.script!, + index: currentDocIndex!, + document: document!, + context: `${type!}_field` as PainlessExecuteContext, + script: script!, documentId: currentDocId, }); @@ -344,8 +334,6 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { return; } - setIsLoadingPreview(false); - const { error: serverError } = response; if (serverError) { @@ -355,39 +343,43 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { }); notifications.toasts.addError(serverError, { title }); + setIsLoadingPreview(false); 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, - }); + if (response.data) { + const { values, error } = response.data; + + if (error) { + setPreviewResponse({ + fields: [{ key: name ?? '', value: '', formattedValue: defaultValueFormatter('') }], + error: { code: 'PAINLESS_SCRIPT_ERROR', error: parseEsError(error) }, + }); + } else { + const [value] = values; + const formattedValue = valueFormatter(value); + + setPreviewResponse({ + fields: [{ key: name!, value, formattedValue }], + error: null, + }); + } } + + setIsLoadingPreview(false); }, [ - needToUpdatePreview, - params, + name, + type, + script, + document, currentDocIndex, currentDocId, getFieldPreview, notifications.toasts, valueFormatter, + allParamsDefined, + scriptEditorValidation, + hasSomeParamsChanged, ]); const goToNextDoc = useCallback(() => { @@ -416,11 +408,6 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { currentIdx: 0, }); setPreviewResponse({ fields: [], error: null }); - setLastExecutePainlessReqParams({ - type: null, - script: undefined, - documentId: undefined, - }); setFrom('cluster'); setIsLoadingPreview(false); setIsFetchingDocument(false); @@ -430,6 +417,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { () => ({ fields: previewResponse.fields, error: previewResponse.error, + isPreviewAvailable, isLoadingPreview, params: { value: params, @@ -437,13 +425,14 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { }, currentDocument: { value: currentDocument, - id: customDocIdToLoad !== null ? customDocIdToLoad : currentDocId, + id: isCustomDocId ? customDocIdToLoad! : currentDocId, isLoading: isFetchingDocument, - isCustomId: customDocIdToLoad !== null, + isCustomId: isCustomDocId, }, documents: { loadSingle: setCustomDocIdToLoad, loadFromCluster: fetchSampleDocuments, + fetchDocError, }, navigation: { isFirstDoc: currentIdx === 0, @@ -464,14 +453,20 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { value: pinnedFields, set: setPinnedFields, }, + validation: { + setScriptEditorValidation, + }, }), [ previewResponse, + fetchDocError, params, + isPreviewAvailable, isLoadingPreview, updateParams, currentDocument, currentDocId, + isCustomDocId, fetchSampleDocuments, isFetchingDocument, customDocIdToLoad, @@ -488,38 +483,23 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { /** * 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 + * the 500ms of the debounce, we set the isLoadingPreview state in this effect whenever + * one of the _execute API param changes */ useEffect(() => { - if (needToUpdatePreview) { + if (allParamsDefined && hasSomeParamsChanged) { setIsLoadingPreview(true); } - }, [needToUpdatePreview, customDocIdToLoad]); + }, [allParamsDefined, hasSomeParamsChanged, script?.source, type, currentDocId]); /** - * Whenever we enter manually a document ID to load we'll clear the - * documents and the preview value. + * In order to immediately display the "Updating..." state indicator and not have to wait + * the 500ms of the debounce, we set the isFetchingDocument state in this effect whenever + * "customDocIdToLoad" changes */ useEffect(() => { - if (customDocIdToLoad !== null) { + if (customDocIdToLoad !== null && Boolean(customDocIdToLoad.trim())) { setIsFetchingDocument(true); - - setClusterData({ - documents: [], - currentIdx: 0, - }); - - setPreviewResponse((prev) => { - const { - fields: { 0: field }, - } = prev; - return { - ...prev, - fields: [ - { ...field, value: undefined, formattedValue: defaultValueFormatter(undefined) }, - ], - }; - }); } }, [customDocIdToLoad]); @@ -566,14 +546,60 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { }); }, [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] - ); + useEffect(() => { + if (script?.source === undefined) { + // Whenever the source is not defined ("Set value" is toggled off or the + // script is empty) we clear the error and update the params cache. + lastExecutePainlessRequestParams.current.script = undefined; + setPreviewError(null); + } + }, [script?.source, setPreviewError]); + + // Handle the validation state coming from the Painless DiagnosticAdapter + // (see @kbn-monaco/src/painless/diagnostics_adapter.ts) + useEffect(() => { + if (scriptEditorValidation.isValidating) { + return; + } + if (scriptEditorValidation.isValid === false) { + // Make sure to remove the "Updating..." spinner + setIsLoadingPreview(false); + + // Set preview response error so it is displayed in the flyout footer + const error = + script?.source === undefined + ? null + : { + code: 'PAINLESS_SYNTAX_ERROR' as const, + error: { + reason: + scriptEditorValidation.message ?? + i18n.translate('indexPatternFieldEditor.fieldPreview.error.painlessSyntax', { + defaultMessage: 'Invalid Painless syntax', + }), + }, + }; + setPreviewError(error); + + // Make sure to update the lastExecutePainlessRequestParams cache so when the user updates + // the script and fixes the syntax the "updatePreview()" will run + lastExecutePainlessRequestParams.current.script = script?.source; + } else { + // Clear possible previous syntax error + clearPreviewError('PAINLESS_SYNTAX_ERROR'); + } + }, [scriptEditorValidation, script?.source, setPreviewError, clearPreviewError]); + + /** + * 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. + */ + useDebounce(updatePreview, 500, [updatePreview]); + + /** + * Whenever the doc ID to load changes we load the document (after a 500ms debounce) + */ useDebounce( () => { if (customDocIdToLoad === null) { 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 index 7994e649e1ebb..6ca38d4d186fb 100644 --- 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 @@ -12,27 +12,25 @@ import { i18n } from '@kbn/i18n'; import { useFieldPreviewContext } from './field_preview_context'; export const FieldPreviewError = () => { - const { error } = useFieldPreviewContext(); + const { + documents: { fetchDocError }, + } = useFieldPreviewContext(); - if (error === null) { + if (fetchDocError === null) { return null; } return ( - {error.code === 'PAINLESS_SCRIPT_ERROR' ? ( -

{error.error.reason}

- ) : ( -

{error.error.message}

- )} +

{fetchDocError.error.message ?? fetchDocError.error.reason}

); }; 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 index 2d3d5c20ba7b3..28b75a43b7d11 100644 --- 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 @@ -7,18 +7,12 @@ */ import React from 'react'; -import { - EuiTitle, - EuiText, - EuiTextColor, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, -} from '@elastic/eui'; +import { EuiTitle, EuiText, EuiTextColor, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useFieldEditorContext } from '../field_editor_context'; import { useFieldPreviewContext } from './field_preview_context'; +import { IsUpdatingIndicator } from './is_updating_indicator'; const i18nTexts = { title: i18n.translate('indexPatternFieldEditor.fieldPreview.title', { @@ -27,21 +21,15 @@ const i18nTexts = { 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 }, + currentDocument: { isLoading: isFetchingDocument }, } = useFieldPreviewContext(); - const isUpdating = isLoadingPreview || isLoading; - return (
@@ -50,15 +38,9 @@ export const FieldPreviewHeader = () => {

{i18nTexts.title}

- - {isUpdating && ( - - - - - - {i18nTexts.updatingLabel} - + {isFetchingDocument && ( + + )}
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 index 5d3b4bb41fc5f..2f93616ef72eb 100644 --- a/src/plugins/index_pattern_field_editor/public/components/preview/index.ts +++ b/src/plugins/index_pattern_field_editor/public/components/preview/index.ts @@ -9,3 +9,5 @@ export { useFieldPreviewContext, FieldPreviewProvider } from './field_preview_context'; export { FieldPreview } from './field_preview'; + +export type { PainlessExecuteContext, FieldPreviewResponse, Context } from './types'; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/is_updating_indicator.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/is_updating_indicator.tsx new file mode 100644 index 0000000000000..0c030d498c617 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/is_updating_indicator.tsx @@ -0,0 +1,27 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +export const IsUpdatingIndicator = () => { + return ( +
+ + + + + + {i18n.translate('indexPatternFieldEditor.fieldPreview.updatingPreviewLabel', { + defaultMessage: 'Updating...', + })} + + +
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/types.ts b/src/plugins/index_pattern_field_editor/public/components/preview/types.ts new file mode 100644 index 0000000000000..d7c0a5867efd6 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/preview/types.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 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 type { RuntimeType, RuntimeField } from '../../shared_imports'; +import type { FieldFormatConfig, RuntimeFieldPainlessError } from '../../types'; + +export type From = 'cluster' | 'custom'; + +export interface EsDocument { + _id: string; + _index: string; + _source: { + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export type ScriptErrorCodes = 'PAINLESS_SCRIPT_ERROR' | 'PAINLESS_SYNTAX_ERROR'; +export type FetchDocErrorCodes = 'DOC_NOT_FOUND' | 'ERR_FETCHING_DOC'; + +interface PreviewError { + code: ScriptErrorCodes; + error: + | RuntimeFieldPainlessError + | { + reason?: string; + [key: string]: unknown; + }; +} + +export interface FetchDocError { + code: FetchDocErrorCodes; + error: { + message?: string; + reason?: string; + [key: string]: unknown; + }; +} + +export interface ClusterData { + documents: EsDocument[]; + currentIdx: number; +} + +// The parameters required to preview the field +export interface Params { + name: string | null; + index: string | null; + type: RuntimeType | null; + script: Required['script'] | null; + format: FieldFormatConfig | null; + document: { [key: string]: unknown } | null; +} + +export interface FieldPreview { + key: string; + value: unknown; + formattedValue?: string; +} + +export interface Context { + fields: FieldPreview[]; + error: PreviewError | null; + params: { + value: Params; + update: (updated: Partial) => void; + }; + isPreviewAvailable: boolean; + isLoadingPreview: boolean; + currentDocument: { + value?: EsDocument; + id?: string; + isLoading: boolean; + isCustomId: boolean; + }; + documents: { + loadSingle: (id: string) => void; + loadFromCluster: () => Promise; + fetchDocError: FetchDocError | null; + }; + 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>; + }; + validation: { + setScriptEditorValidation: React.Dispatch< + React.SetStateAction<{ isValid: boolean; isValidating: boolean; message: string | null }> + >; + }; +} + +export type PainlessExecuteContext = + | 'boolean_field' + | 'date_field' + | 'double_field' + | 'geo_point_field' + | 'ip_field' + | 'keyword_field' + | 'long_field'; + +export interface FieldPreviewResponse { + values: unknown[]; + error?: ScriptError; +} + +export interface ScriptError { + caused_by: { + reason: string; + [key: string]: unknown; + }; + position?: { + offset: number; + start: number; + end: number; + }; + script_stack?: string[]; + [key: string]: unknown; +} diff --git a/src/plugins/index_pattern_field_editor/public/lib/api.ts b/src/plugins/index_pattern_field_editor/public/lib/api.ts index 9641619640a52..594cd07ecb70e 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/api.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/api.ts @@ -8,7 +8,7 @@ import { HttpSetup } from 'src/core/public'; import { API_BASE_PATH } from '../../common/constants'; import { sendRequest } from '../shared_imports'; -import { FieldPreviewContext, FieldPreviewResponse } from '../types'; +import { PainlessExecuteContext, FieldPreviewResponse } from '../components/preview'; export const initApi = (httpClient: HttpSetup) => { const getFieldPreview = ({ @@ -19,7 +19,7 @@ export const initApi = (httpClient: HttpSetup) => { documentId, }: { index: string; - context: FieldPreviewContext; + context: PainlessExecuteContext; script: { source: string } | null; document: Record; documentId: string; 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 d9aaab77ff66a..c7627a63da9ff 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/index.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/index.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -export { deserializeField } from './serialization'; +export { deserializeField, painlessErrorToMonacoMarker } from './serialization'; export { getLinks } from './documentation'; -export type { RuntimeFieldPainlessError } from './runtime_field_validation'; -export { getRuntimeFieldValidator, parseEsError } from './runtime_field_validation'; +export { parseEsError } from './runtime_field_validation'; export type { ApiService } from './api'; + export { initApi } from './api'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts deleted file mode 100644 index b25d47b3d0d15..0000000000000 --- a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts +++ /dev/null @@ -1,165 +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 { dataPluginMock } from '../../../data/public/mocks'; -import { getRuntimeFieldValidator } from './runtime_field_validation'; - -const dataStart = dataPluginMock.createStartContract(); -const { search } = dataStart; - -const runtimeField = { - type: 'keyword', - script: { - source: 'emit("hello")', - }, -}; - -const spy = jest.fn(); - -search.search = () => - ({ - toPromise: spy, - } as any); - -const validator = getRuntimeFieldValidator('myIndex', search); - -describe('Runtime field validation', () => { - const expectedError = { - message: 'Error compiling the painless script', - position: { offset: 4, start: 0, end: 18 }, - reason: 'cannot resolve symbol [emit]', - scriptStack: ["emit.some('value')", ' ^---- HERE'], - }; - - [ - { - title: 'should return null when there are no errors', - response: {}, - status: 200, - expected: null, - }, - { - title: 'should return the error in the first failed shard', - response: { - attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - failed_shards: [ - { - shard: 0, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - type: 'script_exception', - script_stack: ["emit.some('value')", ' ^---- HERE'], - position: { offset: 4, start: 0, end: 18 }, - caused_by: { - type: 'illegal_argument_exception', - reason: 'cannot resolve symbol [emit]', - }, - }, - }, - ], - }, - }, - }, - status: 400, - expected: expectedError, - }, - { - title: 'should return the error in the third failed shard', - response: { - attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - failed_shards: [ - { - shard: 0, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - type: 'foo', - }, - }, - { - shard: 1, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - type: 'bar', - }, - }, - { - shard: 2, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - type: 'script_exception', - script_stack: ["emit.some('value')", ' ^---- HERE'], - position: { offset: 4, start: 0, end: 18 }, - caused_by: { - type: 'illegal_argument_exception', - reason: 'cannot resolve symbol [emit]', - }, - }, - }, - ], - }, - }, - }, - status: 400, - expected: expectedError, - }, - { - title: 'should have default values if an error prop is not found', - response: { - attributes: { - type: 'status_exception', - reason: 'error while executing search', - caused_by: { - failed_shards: [ - { - shard: 0, - index: 'kibana_sample_data_logs', - node: 'gVwk20UWSdO6VyuNOc_6UA', - reason: { - // script_stack, position and caused_by are missing - type: 'script_exception', - caused_by: { - type: 'illegal_argument_exception', - }, - }, - }, - ], - }, - }, - }, - status: 400, - expected: { - message: 'Error compiling the painless script', - position: null, - reason: null, - scriptStack: [], - }, - }, - ].map(({ title, response, status, expected }) => { - test(title, async () => { - if (status !== 200) { - spy.mockRejectedValueOnce(response); - } else { - spy.mockResolvedValueOnce(response); - } - - const result = await validator(runtimeField); - - expect(result).toEqual(expected); - }); - }); -}); 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 5f80b7823b6a0..770fb548f1251 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 @@ -6,72 +6,28 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; +import { ScriptError } from '../components/preview/types'; +import { RuntimeFieldPainlessError, PainlessErrorCode } from '../types'; -import { DataPublicPluginStart } from '../shared_imports'; -import type { EsRuntimeField } from '../types'; - -export interface RuntimeFieldPainlessError { - message: string; - reason: string; - position: { - offset: number; - start: number; - end: number; - } | null; - scriptStack: string[]; -} - -type Error = Record; - -/** - * We are only interested in "script_exception" error type - */ -const getScriptExceptionErrorOnShard = (error: Error): Error | null => { - if (error.type === 'script_exception') { - return error; - } - - if (!error.caused_by) { - return null; +export const getErrorCodeFromErrorReason = (reason: string = ''): PainlessErrorCode => { + if (reason.startsWith('Cannot cast from')) { + return 'CAST_ERROR'; } - - // Recursively try to get a script exception error - return getScriptExceptionErrorOnShard(error.caused_by); + return 'UNKNOWN'; }; -/** - * We get the first script exception error on any failing shard. - * The UI can only display one error at the time so there is no need - * to look any further. - */ -const getScriptExceptionError = (error: Error): Error | null => { - if (error === undefined || !Array.isArray(error.failed_shards)) { - return null; - } +export const parseEsError = (scriptError: ScriptError): RuntimeFieldPainlessError => { + let reason = scriptError.caused_by?.reason; + const errorCode = getErrorCodeFromErrorReason(reason); - let scriptExceptionError = null; - for (const err of error.failed_shards) { - scriptExceptionError = getScriptExceptionErrorOnShard(err.reason); - - if (scriptExceptionError !== null) { - break; - } - } - return scriptExceptionError; -}; - -export const parseEsError = ( - error?: Error, - isScriptError = false -): RuntimeFieldPainlessError | null => { - if (error === undefined) { - return null; - } - - const scriptError = isScriptError ? error : getScriptExceptionError(error.caused_by); - - if (scriptError === null) { - return null; + if (errorCode === 'CAST_ERROR') { + // Help the user as he might have forgot to change the runtime type + reason = `${reason} ${i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditor.castErrorMessage', + { + defaultMessage: 'Verify that you have correctly set the runtime field type.', + } + )}`; } return { @@ -83,36 +39,7 @@ export const parseEsError = ( ), position: scriptError.position ?? null, scriptStack: scriptError.script_stack ?? [], - reason: scriptError.caused_by?.reason ?? null, + reason: reason ?? null, + code: errorCode, }; }; - -/** - * Handler to validate the painless script for syntax and semantic errors. - * This is a temporary solution. In a future work we will have a dedicate - * ES API to debug the script. - */ -export const getRuntimeFieldValidator = - (index: string, searchService: DataPublicPluginStart['search']) => - async (runtimeField: EsRuntimeField) => { - return await searchService - .search({ - params: { - index, - body: { - runtime_mappings: { - temp: runtimeField, - }, - size: 0, - query: { - match_none: {}, - }, - }, - }, - }) - .toPromise() - .then(() => null) - .catch((e) => { - return parseEsError(e.attributes); - }); - }; 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 8a0a47e07c9c9..0f042cdac114f 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts @@ -5,9 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { monaco } from '@kbn/monaco'; import { IndexPatternField, IndexPattern } from '../shared_imports'; -import type { Field } from '../types'; +import type { Field, RuntimeFieldPainlessError } from '../types'; export const deserializeField = ( indexPattern: IndexPattern, @@ -26,3 +26,20 @@ export const deserializeField = ( format: indexPattern.getFormatterForFieldNoDefault(field.name)?.toJSON(), }; }; + +export const painlessErrorToMonacoMarker = ( + { reason }: RuntimeFieldPainlessError, + startPosition: monaco.Position +): monaco.editor.IMarkerData | undefined => { + return { + startLineNumber: startPosition.lineNumber, + startColumn: startPosition.column, + endLineNumber: startPosition.lineNumber, + // Ideally we'd want the endColumn to be the end of the error but + // ES does not return that info. There is an issue to track the enhancement: + // https://github.com/elastic/elasticsearch/issues/78072 + endColumn: startPosition.column + 1, + message: reason, + severity: monaco.MarkerSeverity.Error, + }; +}; 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 e2154800908cb..5b377bdd1d2b5 100644 --- a/src/plugins/index_pattern_field_editor/public/shared_imports.ts +++ b/src/plugins/index_pattern_field_editor/public/shared_imports.ts @@ -23,7 +23,9 @@ export type { FormHook, ValidationFunc, FieldConfig, + ValidationCancelablePromise, } from '../../es_ui_shared/static/forms/hook_form_lib'; + export { useForm, useFormData, @@ -31,6 +33,7 @@ export { useFormIsModified, Form, UseField, + useBehaviorSubject, } from '../../es_ui_shared/static/forms/hook_form_lib'; export { fieldValidators } from '../../es_ui_shared/static/forms/helpers'; diff --git a/src/plugins/index_pattern_field_editor/public/types.ts b/src/plugins/index_pattern_field_editor/public/types.ts index f7efc9d82fc48..9d62a5568584c 100644 --- a/src/plugins/index_pattern_field_editor/public/types.ts +++ b/src/plugins/index_pattern_field_editor/public/types.ts @@ -66,16 +66,24 @@ 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 type PainlessErrorCode = 'CAST_ERROR' | 'UNKNOWN'; -export interface FieldPreviewResponse { - values: unknown[]; - error?: Record; +export interface RuntimeFieldPainlessError { + message: string; + reason: string; + position: { + offset: number; + start: number; + end: number; + } | null; + scriptStack: string[]; + code: PainlessErrorCode; +} + +export interface MonacoEditorErrorMarker { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + message: string; } 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 index 9ffa5c88df8e8..e95c12469ffb9 100644 --- a/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts +++ b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts @@ -58,6 +58,13 @@ export const registerFieldPreviewRoute = ({ router }: RouteDependencies): void = }; try { + // Ideally we want to use the Painless _execute API to get the runtime field preview. + // There is a current ES limitation that requires a user to have too many privileges + // to execute the script. (issue: https://github.com/elastic/elasticsearch/issues/48856) + // Until we find a way to execute a script without advanced privileges we are going to + // use the Search API to get the field value (and possible errors). + // Note: here is the PR were we changed from using Painless _execute to _search and should be + // reverted when the ES issue is fixed: https://github.com/elastic/kibana/pull/115070 const response = await client.asCurrentUser.search({ index: req.body.index, body, 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 index 7123be1deb18a..c687f3094b6fd 100644 --- a/test/api_integration/apis/index_pattern_field_editor/field_preview.ts +++ b/test/api_integration/apis/index_pattern_field_editor/field_preview.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; +import { getErrorCodeFromErrorReason } from '../../../../src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation'; import { FtrProviderContext } from '../../ftr_provider_context'; import { API_BASE_PATH } from './constants'; @@ -140,5 +141,26 @@ export default function ({ getService }: FtrProviderContext) { .expect(400); }); }); + + describe('Error messages', () => { + // As ES does not return error codes we will add a test to make sure its error message string + // does not change overtime as we rely on it to extract our own error code. + // If this test fail we'll need to update the "getErrorCodeFromErrorReason()" handler + it('should detect a script casting error', async () => { + const { body: response } = await supertest + .post(`${API_BASE_PATH}/field_preview`) + .send({ + script: { source: 'emit(123)' }, // We send a long but the type is "keyword" + context: 'keyword_field', + index: INDEX_NAME, + documentId: DOC_ID, + }) + .set('kbn-xsrf', 'xxx'); + + const errorCode = getErrorCodeFromErrorReason(response.error?.caused_by?.reason); + + expect(errorCode).be('CAST_ERROR'); + }); + }); }); } diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index 0618dd79e272e..1a71e4c5fbc68 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -33,7 +33,7 @@ export default function ({ getService, getPageObjects }) { }); afterEach(async () => { - await testSubjects.click('closeFlyoutButton'); + await PageObjects.settings.closeIndexPatternFieldEditor(); await PageObjects.settings.removeIndexPattern(); // Cancel saving the popularity change (we didn't make a change in this case, just checking the value) }); @@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }) { it('should be reset on cancel', async function () { // Cancel saving the popularity change - await testSubjects.click('closeFlyoutButton'); + await PageObjects.settings.closeIndexPatternFieldEditor(); await PageObjects.settings.openControlsByName(fieldName); // check that it is 0 (previous increase was cancelled const popularity = await PageObjects.settings.getPopularity(); diff --git a/test/functional/services/field_editor.ts b/test/functional/services/field_editor.ts index 27cb8cf010d92..d6d2f2606e29d 100644 --- a/test/functional/services/field_editor.ts +++ b/test/functional/services/field_editor.ts @@ -37,7 +37,13 @@ export class FieldEditorService extends FtrService { const textarea = await editor.findByClassName('monaco-mouse-cursor-text'); await textarea.click(); - await this.browser.pressKeys(script); + + // To avoid issue with the timing needed for Selenium to write the script and the monaco editor + // syntax validation kicking in, we loop through all the chars of the script and enter + // them one by one (instead of calling "await this.browser.pressKeys(script);"). + for (const letter of script.split('')) { + await this.browser.pressKeys(letter); + } } public async save() { await this.testSubjects.click('fieldSaveButton'); diff --git a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx index 5a40be95dd824..76805c452bf98 100644 --- a/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx +++ b/x-pack/plugins/osquery/public/packs/queries/ecs_mapping_editor_field.tsx @@ -628,13 +628,13 @@ export const ECSMappingEditorForm = forwardRef { validate(); - __validateFields(['result.value']); + validateFields(['result.value']); const { data, isValid } = await submit(); if (isValid) { @@ -652,7 +652,7 @@ export const ECSMappingEditorForm = forwardRef { if (defaultValue?.key && onDelete) { @@ -701,7 +701,7 @@ export const ECSMappingEditorForm = forwardRef { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 107ff1bc546dc..a7b734ba20662 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3799,9 +3799,7 @@ "indexPatternFieldEditor.editor.form.runtimeTypeLabel": "型", "indexPatternFieldEditor.editor.form.script.learnMoreLinkText": "スクリプト構文の詳細を参照してください。", "indexPatternFieldEditor.editor.form.scriptEditor.compileErrorMessage": "Painlessスクリプトのコンパイルエラー", - "indexPatternFieldEditor.editor.form.scriptEditor.debugErrorMessage": "構文エラー詳細", "indexPatternFieldEditor.editor.form.scriptEditorAriaLabel": "スクリプトエディター", - "indexPatternFieldEditor.editor.form.scriptEditorValidationMessage": "無効なPainless構文です。", "indexPatternFieldEditor.editor.form.source.scriptFieldHelpText": "スクリプトがないランタイムフィールドは、{source}から値を取得します。フィールドが_sourceに存在しない場合は、検索リクエストは値を返しません。{learnMoreLink}", "indexPatternFieldEditor.editor.form.typeSelectAriaLabel": "タイプ選択", "indexPatternFieldEditor.editor.form.validations.customLabelIsRequiredErrorMessage": "フィールドにラベルを付けます。", @@ -3812,9 +3810,6 @@ "indexPatternFieldEditor.editor.form.valueDescription": "{source}の同じ名前のフィールドから取得するのではなく、フィールドの値を設定します。", "indexPatternFieldEditor.editor.form.valueTitle": "値を設定", "indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage": "この名前のフィールドはすでに存在します。", - "indexPatternFieldEditor.editor.validationErrorTitle": "続行する前にフォームのエラーを修正してください。", - "indexPatternFieldEditor.fieldPreview.defaultErrorTitle": "指定したスクリプトを実行できません", - "indexPatternFieldEditor.fieldPreview.documentIdField.documentNotFoundError": "ドキュメントが見つかりません", "indexPatternFieldEditor.fieldPreview.documentIdField.label": "ドキュメントID", "indexPatternFieldEditor.fieldPreview.documentIdField.loadDocumentsFromCluster": "クラスターからドキュメントを読み込む", "indexPatternFieldEditor.fieldPreview.documentNav.nextArialabel": "次のドキュメント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 31f44408917c5..f5b364b35fa13 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3835,9 +3835,7 @@ "indexPatternFieldEditor.editor.form.runtimeTypeLabel": "类型", "indexPatternFieldEditor.editor.form.script.learnMoreLinkText": "了解脚本语法。", "indexPatternFieldEditor.editor.form.scriptEditor.compileErrorMessage": "编译 Painless 脚本时出错", - "indexPatternFieldEditor.editor.form.scriptEditor.debugErrorMessage": "语法错误详细信息", "indexPatternFieldEditor.editor.form.scriptEditorAriaLabel": "脚本编辑器", - "indexPatternFieldEditor.editor.form.scriptEditorValidationMessage": "Painless 语法无效。", "indexPatternFieldEditor.editor.form.source.scriptFieldHelpText": "没有脚本的运行时字段从 {source} 中检索值。如果字段在 _source 中不存在,搜索请求将不返回值。{learnMoreLink}", "indexPatternFieldEditor.editor.form.typeSelectAriaLabel": "类型选择", "indexPatternFieldEditor.editor.form.validations.customLabelIsRequiredErrorMessage": "为字段提供标签。", @@ -3848,9 +3846,6 @@ "indexPatternFieldEditor.editor.form.valueDescription": "为字段设置值,而非从在 {source} 中同名的字段检索值。", "indexPatternFieldEditor.editor.form.valueTitle": "设置值", "indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage": "已存在具有此名称的字段。", - "indexPatternFieldEditor.editor.validationErrorTitle": "继续前请解决表单中的错误。", - "indexPatternFieldEditor.fieldPreview.defaultErrorTitle": "无法运行提供的脚本", - "indexPatternFieldEditor.fieldPreview.documentIdField.documentNotFoundError": "未找到文档", "indexPatternFieldEditor.fieldPreview.documentIdField.label": "文档 ID", "indexPatternFieldEditor.fieldPreview.documentIdField.loadDocumentsFromCluster": "从集群加载文档", "indexPatternFieldEditor.fieldPreview.documentNav.nextArialabel": "下一个文档", From 09523079c1eddc34ab0d759a4a527c0986962108 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 9 Nov 2021 10:52:38 -0500 Subject: [PATCH 41/98] fix flaky test + bug (#117953) --- x-pack/test/functional/apps/spaces/copy_saved_objects.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts index 807e0fff16ff1..2d2fdf61a94b6 100644 --- a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts +++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts @@ -17,8 +17,7 @@ export default function spaceSelectorFunctonalTests({ const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['security', 'settings', 'copySavedObjectsToSpace']); - // FLAKY: https://github.com/elastic/kibana/issues/44575 - describe.skip('Copy Saved Objects to Space', function () { + describe('Copy Saved Objects to Space', function () { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/spaces/copy_saved_objects'); From c32191007d499d6bf3675b1fdb7b393b27bd43c6 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 9 Nov 2021 10:53:12 -0500 Subject: [PATCH 42/98] [SECURITY] Remove flakiness on SpacesPopoverList renders a search box (#117888) * wip to figure out flakyness * clean up * fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../spaces_popover_list.test.tsx | 34 +---- .../apps/spaces/spaces_selection.ts | 75 +++++++++- .../es_archives/spaces/selector/data.json | 137 +++++++++++++++++- .../page_objects/space_selector_page.ts | 26 ++++ 4 files changed, 235 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx index fb21fac3006b8..031df26eb38f7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx @@ -46,8 +46,7 @@ const spacesManager = spacesManagerMock.create(); const { getStartServices } = coreMock.createSetup(); const spacesApiUi = getUiApi({ spacesManager, getStartServices }); -// FLAKY: https://github.com/elastic/kibana/issues/101454 -describe.skip('SpacesPopoverList', () => { +describe('SpacesPopoverList', () => { async function setup(spaces: Space[]) { const wrapper = mountWithIntl( @@ -84,41 +83,16 @@ describe.skip('SpacesPopoverList', () => { const spaceAvatar = items.at(index).find(SpaceAvatarInternal); expect(spaceAvatar.props().space).toEqual(space); }); - - expect(wrapper.find(EuiFieldSearch)).toHaveLength(0); }); - it('renders a search box when there are 8 or more spaces', async () => { - const lotsOfSpaces = [1, 2, 3, 4, 5, 6, 7, 8].map((num) => ({ - id: `space-${num}`, - name: `Space ${num}`, - disabledFeatures: [], - })); - - const wrapper = await setup(lotsOfSpaces); + it('Should NOT render a search box when there is less than 8 spaces', async () => { + const wrapper = await setup(mockSpaces); await act(async () => { wrapper.find(EuiButtonEmpty).simulate('click'); }); wrapper.update(); - const menu = wrapper.find(EuiContextMenuPanel).first(); - const items = menu.find(EuiContextMenuItem); - expect(items).toHaveLength(lotsOfSpaces.length); - - const searchField = wrapper.find(EuiFieldSearch); - expect(searchField).toHaveLength(1); - - searchField.props().onSearch!('Space 6'); - await act(async () => {}); - wrapper.update(); - expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(1); - - searchField.props().onSearch!('this does not match'); - wrapper.update(); - expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(0); - - const updatedMenu = wrapper.find(EuiContextMenuPanel).first(); - expect(updatedMenu.text()).toMatchInlineSnapshot(`"Spaces no spaces found "`); + expect(wrapper.find(EuiFieldSearch)).toHaveLength(0); }); it('can close its popover', async () => { diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index ef554bb80ebc4..4344f8ff61733 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -23,15 +23,18 @@ export default function spaceSelectorFunctionalTests({ ]); describe('Spaces', function () { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/spaces/selector'); + }); + after( + async () => await esArchiver.unload('x-pack/test/functional/es_archives/spaces/selector') + ); + this.tags('includeFirefox'); describe('Space Selector', () => { before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/spaces/selector'); await PageObjects.security.forceLogout(); }); - after( - async () => await esArchiver.unload('x-pack/test/functional/es_archives/spaces/selector') - ); afterEach(async () => { // NOTE: Logout needs to happen before anything else to avoid flaky behavior @@ -59,6 +62,68 @@ export default function spaceSelectorFunctionalTests({ }); }); + describe('Space Selector', () => { + before(async () => { + await PageObjects.security.forceLogout(); + }); + + afterEach(async () => { + await PageObjects.security.forceLogout(); + }); + + it('allows user to navigate to different spaces', async () => { + const spaceId = 'another-space'; + + await PageObjects.security.login(undefined, undefined, { + expectSpaceSelector: true, + }); + + await PageObjects.spaceSelector.clickSpaceCard(spaceId); + + await PageObjects.spaceSelector.expectHomePage(spaceId); + + await PageObjects.spaceSelector.openSpacesNav(); + + // change spaces + + await PageObjects.spaceSelector.clickSpaceAvatar('default'); + + await PageObjects.spaceSelector.expectHomePage('default'); + }); + }); + + describe('Search spaces in popover', () => { + const spaceId = 'default'; + before(async () => { + await PageObjects.security.forceLogout(); + await PageObjects.security.login(undefined, undefined, { + expectSpaceSelector: true, + }); + }); + + after(async () => { + await PageObjects.security.forceLogout(); + }); + + it('allows user to search for spaces', async () => { + await PageObjects.spaceSelector.clickSpaceCard(spaceId); + await PageObjects.spaceSelector.expectHomePage(spaceId); + await PageObjects.spaceSelector.openSpacesNav(); + await PageObjects.spaceSelector.expectSearchBoxInSpacesSelector(); + }); + + it('search for "ce 1" and find one space', async () => { + await PageObjects.spaceSelector.setSearchBoxInSpacesSelector('ce 1'); + await PageObjects.spaceSelector.expectToFindThatManySpace(1); + }); + + it('search for "dog" and find NO space', async () => { + await PageObjects.spaceSelector.setSearchBoxInSpacesSelector('dog'); + await PageObjects.spaceSelector.expectToFindThatManySpace(0); + await PageObjects.spaceSelector.expectNoSpacesFound(); + }); + }); + describe('Spaces Data', () => { const spaceId = 'another-space'; const sampleDataHash = '/tutorial_directory/sampleData'; @@ -71,7 +136,6 @@ export default function spaceSelectorFunctionalTests({ }; before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/spaces/selector'); await PageObjects.security.login(undefined, undefined, { expectSpaceSelector: true, }); @@ -97,7 +161,6 @@ export default function spaceSelectorFunctionalTests({ }); await PageObjects.home.removeSampleDataSet('logs'); await PageObjects.security.forceLogout(); - await esArchiver.unload('x-pack/test/functional/es_archives/spaces/selector'); }); describe('displays separate data for each space', () => { diff --git a/x-pack/test/functional/es_archives/spaces/selector/data.json b/x-pack/test/functional/es_archives/spaces/selector/data.json index 7daf9561616ee..43fbca65865b6 100644 --- a/x-pack/test/functional/es_archives/spaces/selector/data.json +++ b/x-pack/test/functional/es_archives/spaces/selector/data.json @@ -41,4 +41,139 @@ "type": "space" } } -} \ No newline at end of file +} + +{ + "type": "doc", + "value": { + "id": "space:space-1", + "index": ".kibana", + "source": { + "space": { + "description": "This is space I", + "name": "Space 1" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:space-2", + "index": ".kibana", + "source": { + "space": { + "description": "This is space II", + "name": "Space 2" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:space-3", + "index": ".kibana", + "source": { + "space": { + "description": "This is space III", + "name": "Space 3" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:space-4", + "index": ".kibana", + "source": { + "space": { + "description": "This is space IV", + "name": "Space 4" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:space-5", + "index": ".kibana", + "source": { + "space": { + "description": "This is space V", + "name": "Space 5" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:space-6", + "index": ".kibana", + "source": { + "space": { + "description": "This is space VI", + "name": "Space 6" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:space-7", + "index": ".kibana", + "source": { + "space": { + "description": "This is space VII", + "name": "Space 7" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:space-8", + "index": ".kibana", + "source": { + "space": { + "description": "This is space VIII", + "name": "Space 8" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:space-9", + "index": ".kibana", + "source": { + "space": { + "description": "This is space IX", + "name": "Space 9" + }, + "type": "space" + } + } +} diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts index 313916fd3b07e..f60da33763240 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.ts +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -21,6 +21,7 @@ export class SpaceSelectorPageObject extends FtrService { } async clickSpaceCard(spaceId: string) { + await this.common.sleep(10000); return await this.retry.try(async () => { this.log.info(`SpaceSelectorPage:clickSpaceCard(${spaceId})`); await this.testSubjects.click(`space-card-${spaceId}`); @@ -189,4 +190,29 @@ export class SpaceSelectorPageObject extends FtrService { await this.common.sleep(1000); }); } + + async expectSearchBoxInSpacesSelector() { + expect(await this.find.existsByCssSelector('div[role="dialog"] input[type="search"]')).to.be( + true + ); + } + + async setSearchBoxInSpacesSelector(searchText: string) { + const searchBox = await this.find.byCssSelector('div[role="dialog"] input[type="search"]'); + searchBox.clearValue(); + searchBox.type(searchText); + await this.common.sleep(1000); + } + + async expectToFindThatManySpace(numberOfExpectedSpace: number) { + const spacesFound = await this.find.allByCssSelector('div[role="dialog"] a.euiContextMenuItem'); + expect(spacesFound.length).to.be(numberOfExpectedSpace); + } + + async expectNoSpacesFound() { + const msgElem = await this.find.byCssSelector( + 'div[role="dialog"] .euiContextMenuPanel .euiText' + ); + expect(await msgElem.getVisibleText()).to.be('no spaces found'); + } } From 90395c5589c055432190848be5880d3c271e9e4b Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 9 Nov 2021 16:58:55 +0100 Subject: [PATCH 43/98] [Lens] Improve outside label placement for pie/donut charts (#115966) * :bug: Shrink pie/donut chart to allow more space for tiny slices * :white_check_mark: Add unit tests * :sparkles: Handler for small slices in Visualize pie Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vis_types/pie/public/pie_component.tsx | 16 ++++- .../pie/public/utils/get_config.test.ts | 66 +++++++++++++++++++ .../vis_types/pie/public/utils/get_config.ts | 7 +- .../render_function.test.tsx | 45 +++++++++++++ .../pie_visualization/render_function.tsx | 10 +++ 5 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 src/plugins/vis_types/pie/public/utils/get_config.test.ts diff --git a/src/plugins/vis_types/pie/public/pie_component.tsx b/src/plugins/vis_types/pie/public/pie_component.tsx index 9211274a8abc8..053d06bb84e29 100644 --- a/src/plugins/vis_types/pie/public/pie_component.tsx +++ b/src/plugins/vis_types/pie/public/pie_component.tsx @@ -234,9 +234,21 @@ const PieComponent = (props: PieComponentProps) => { syncColors, ] ); + + const rescaleFactor = useMemo(() => { + const overallSum = visData.rows.reduce((sum, row) => sum + row[metricColumn.id], 0); + const slices = visData.rows.map((row) => row[metricColumn.id] / overallSum); + const smallSlices = slices.filter((value) => value < 0.02).length; + if (smallSlices) { + // shrink up to 20% to give some room for the linked values + return 1 / (1 + Math.min(smallSlices * 0.05, 0.2)); + } + return 1; + }, [visData.rows, metricColumn]); + const config = useMemo( - () => getConfig(visParams, chartTheme, dimensions), - [chartTheme, visParams, dimensions] + () => getConfig(visParams, chartTheme, dimensions, rescaleFactor), + [chartTheme, visParams, dimensions, rescaleFactor] ); const tooltip: TooltipProps = { type: visParams.addTooltip ? TooltipType.Follow : TooltipType.None, diff --git a/src/plugins/vis_types/pie/public/utils/get_config.test.ts b/src/plugins/vis_types/pie/public/utils/get_config.test.ts new file mode 100644 index 0000000000000..82907002a19d5 --- /dev/null +++ b/src/plugins/vis_types/pie/public/utils/get_config.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { getConfig } from './get_config'; +import { createMockPieParams } from '../mocks'; + +const visParams = createMockPieParams(); + +describe('getConfig', () => { + it('should cap the outerSizeRatio to 1', () => { + expect(getConfig(visParams, {}, { width: 400, height: 400 }).outerSizeRatio).toBe(1); + }); + + it('should not have outerSizeRatio for split chart', () => { + expect( + getConfig( + { + ...visParams, + dimensions: { + ...visParams.dimensions, + splitColumn: [ + { + accessor: 1, + format: { + id: 'number', + }, + }, + ], + }, + }, + {}, + { width: 400, height: 400 } + ).outerSizeRatio + ).toBeUndefined(); + + expect( + getConfig( + { + ...visParams, + dimensions: { + ...visParams.dimensions, + splitRow: [ + { + accessor: 1, + format: { + id: 'number', + }, + }, + ], + }, + }, + {}, + { width: 400, height: 400 } + ).outerSizeRatio + ).toBeUndefined(); + }); + + it('should not set outerSizeRatio if dimensions are not defined', () => { + expect(getConfig(visParams, {}).outerSizeRatio).toBeUndefined(); + }); +}); diff --git a/src/plugins/vis_types/pie/public/utils/get_config.ts b/src/plugins/vis_types/pie/public/utils/get_config.ts index 40f8f84b127f9..9f67155145820 100644 --- a/src/plugins/vis_types/pie/public/utils/get_config.ts +++ b/src/plugins/vis_types/pie/public/utils/get_config.ts @@ -13,7 +13,8 @@ const MAX_SIZE = 1000; export const getConfig = ( visParams: PieVisParams, chartTheme: RecursivePartial, - dimensions?: PieContainerDimensions + dimensions?: PieContainerDimensions, + rescaleFactor: number = 1 ): RecursivePartial => { // On small multiples we want the labels to only appear inside const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow); @@ -32,7 +33,9 @@ export const getConfig = ( const usingOuterSizeRatio = dimensions && !isSplitChart ? { - outerSizeRatio: MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), + outerSizeRatio: + // Cap the ratio to 1 and then rescale + rescaleFactor * Math.min(MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), 1), } : null; const config: RecursivePartial = { diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index ad4e30cd6e89f..55b621498bb10 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -376,5 +376,50 @@ describe('PieVisualization component', () => { expect(component.find(VisualizationContainer)).toHaveLength(1); expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDonut); }); + + test('it should dynamically shrink the chart area to when some small slices are detected', () => { + const defaultData = getDefaultArgs().data; + const emptyData: LensMultiTable = { + ...defaultData, + tables: { + first: { + ...defaultData.tables.first, + rows: [ + { a: 60, b: 'I', c: 200, d: 'Row 1' }, + { a: 1, b: 'J', c: 0.1, d: 'Row 2' }, + ], + }, + }, + }; + + const component = shallow( + + ); + expect(component.find(Partition).prop('config')?.outerSizeRatio).toBeCloseTo(1 / 1.05); + }); + + test('it should bound the shrink the chart area to ~20% when some small slices are detected', () => { + const defaultData = getDefaultArgs().data; + const emptyData: LensMultiTable = { + ...defaultData, + tables: { + first: { + ...defaultData.tables.first, + rows: [ + { a: 60, b: 'I', c: 200, d: 'Row 1' }, + { a: 1, b: 'J', c: 0.1, d: 'Row 2' }, + { a: 1, b: 'K', c: 0.1, d: 'Row 3' }, + { a: 1, b: 'G', c: 0.1, d: 'Row 4' }, + { a: 1, b: 'H', c: 0.1, d: 'Row 5' }, + ], + }, + }, + }; + + const component = shallow( + + ); + expect(component.find(Partition).prop('config')?.outerSizeRatio).toBeCloseTo(1 / 1.2); + }); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 449b152523881..070448978f4ef 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -204,6 +204,16 @@ export function PieComponent( } else if (categoryDisplay === 'inside') { // Prevent links from showing config.linkLabel = { maxCount: 0 }; + } else { + // if it contains any slice below 2% reduce the ratio + // first step: sum it up the overall sum + const overallSum = firstTable.rows.reduce((sum, row) => sum + row[metric!], 0); + const slices = firstTable.rows.map((row) => row[metric!] / overallSum); + const smallSlices = slices.filter((value) => value < 0.02).length; + if (smallSlices) { + // shrink up to 20% to give some room for the linked values + config.outerSizeRatio = 1 / (1 + Math.min(smallSlices * 0.05, 0.2)); + } } } const metricColumn = firstTable.columns.find((c) => c.id === metric)!; From 69c0e6c6013862a7cedcd3bf13ee5a9e5fc683c4 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 9 Nov 2021 08:00:39 -0800 Subject: [PATCH 44/98] Improve Clean Repository error message. (#117947) --- .../helpers/home.helpers.ts | 270 +----------------- .../helpers/http_requests.ts | 3 +- .../__jest__/client_integration/home.test.ts | 54 ++++ .../repository_details/repository_details.tsx | 25 +- .../server/lib/wrap_es_error.ts | 2 +- .../server/routes/api/repositories.ts | 21 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 8 files changed, 83 insertions(+), 294 deletions(-) diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts index 238d3e0440493..71930efe12953 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts @@ -22,7 +22,7 @@ const testBedConfig: TestBedConfig = { const initTestBed = registerTestBed(WithAppDependencies(SnapshotRestoreHome), testBedConfig); -export interface HomeTestBed extends TestBed { +export interface HomeTestBed extends TestBed { actions: { clickReloadButton: () => void; selectRepositoryAt: (index: number) => void; @@ -115,271 +115,3 @@ export const setup = async (): Promise => { }, }; }; - -type HomeTestSubjects = TestSubjects | ThreeLevelDepth | NonVisibleTestSubjects; - -type NonVisibleTestSubjects = - | 'snapshotDetail.sectionLoading' - | 'sectionLoading' - | 'emptyPrompt' - | 'emptyPrompt.documentationLink' - | 'emptyPrompt.title' - | 'emptyPrompt.registerRepositoryButton' - | 'repositoryDetail.sectionLoading' - | 'snapshotDetail.indexFailure'; - -type ThreeLevelDepth = - | 'snapshotDetail.uuid.value' - | 'snapshotDetail.state.value' - | 'snapshotDetail.version.value' - | 'snapshotDetail.includeGlobalState.value' - | 'snapshotDetail.indices.title' - | 'snapshotDetail.startTime.value' - | 'snapshotDetail.endTime.value' - | 'snapshotDetail.indexFailure.index' - | 'snapshotDetail.indices.value'; - -export type TestSubjects = - | 'appTitle' - | 'cell' - | 'cell.repositoryLink' - | 'cell.snapshotLink' - | 'checkboxSelectAll' - | 'checkboxSelectRow-my-repo' - | 'closeButton' - | 'content' - | 'content.documentationLink' - | 'content.duration' - | 'content.endTime' - | 'content.includeGlobalState' - | 'content.indices' - | 'content.repositoryType' - | 'content.snapshotCount' - | 'content.startTime' - | 'content.state' - | 'content.title' - | 'content.uuid' - | 'content.value' - | 'content.verifyRepositoryButton' - | 'content.version' - | 'deleteRepositoryButton' - | 'detailTitle' - | 'documentationLink' - | 'duration' - | 'duration.title' - | 'duration.value' - | 'editRepositoryButton' - | 'endTime' - | 'endTime.title' - | 'endTime.value' - | 'euiFlyoutCloseButton' - | 'includeGlobalState' - | 'includeGlobalState.title' - | 'includeGlobalState.value' - | 'indices' - | 'indices.title' - | 'indices.value' - | 'registerRepositoryButton' - | 'reloadButton' - | 'repositoryDetail' - | 'repositoryDetail.content' - | 'repositoryDetail.documentationLink' - | 'repositoryDetail.euiFlyoutCloseButton' - | 'repositoryDetail.repositoryType' - | 'repositoryDetail.snapshotCount' - | 'repositoryDetail.srRepositoryDetailsDeleteActionButton' - | 'repositoryDetail.srRepositoryDetailsFlyoutCloseButton' - | 'repositoryDetail.title' - | 'repositoryDetail.verifyRepositoryButton' - | 'repositoryLink' - | 'repositoryList' - | 'repositoryList.cell' - | 'repositoryList.checkboxSelectAll' - | 'repositoryList.checkboxSelectRow-my-repo' - | 'repositoryList.content' - | 'repositoryList.deleteRepositoryButton' - | 'repositoryList.documentationLink' - | 'repositoryList.editRepositoryButton' - | 'repositoryList.euiFlyoutCloseButton' - | 'repositoryList.registerRepositoryButton' - | 'repositoryList.reloadButton' - | 'repositoryList.repositoryDetail' - | 'repositoryList.repositoryLink' - | 'repositoryList.repositoryTable' - | 'repositoryList.repositoryType' - | 'repositoryList.row' - | 'repositoryList.snapshotCount' - | 'repositoryList.srRepositoryDetailsDeleteActionButton' - | 'repositoryList.srRepositoryDetailsFlyoutCloseButton' - | 'repositoryList.tableHeaderCell_name_0' - | 'repositoryList.tableHeaderCell_type_1' - | 'repositoryList.tableHeaderSortButton' - | 'repositoryList.title' - | 'repositoryList.verifyRepositoryButton' - | 'repositoryTable' - | 'repositoryTable.cell' - | 'repositoryTable.checkboxSelectAll' - | 'repositoryTable.checkboxSelectRow-my-repo' - | 'repositoryTable.deleteRepositoryButton' - | 'repositoryTable.editRepositoryButton' - | 'repositoryTable.repositoryLink' - | 'repositoryTable.row' - | 'repositoryTable.tableHeaderCell_name_0' - | 'repositoryTable.tableHeaderCell_type_1' - | 'repositoryTable.tableHeaderSortButton' - | 'repositoryType' - | 'row' - | 'row.cell' - | 'row.checkboxSelectRow-my-repo' - | 'row.deleteRepositoryButton' - | 'row.editRepositoryButton' - | 'row.repositoryLink' - | 'row.snapshotLink' - | 'snapshotCount' - | 'snapshotDetail' - | 'snapshotDetail.closeButton' - | 'snapshotDetail.content' - | 'snapshotDetail.detailTitle' - | 'snapshotDetail.duration' - | 'snapshotDetail.endTime' - | 'snapshotDetail.euiFlyoutCloseButton' - | 'snapshotDetail.includeGlobalState' - | 'snapshotDetail.indices' - | 'snapshotDetail.repositoryLink' - | 'snapshotDetail.startTime' - | 'snapshotDetail.state' - | 'snapshotDetail.tab' - | 'snapshotDetail.title' - | 'snapshotDetail.uuid' - | 'snapshotDetail.value' - | 'snapshotDetail.version' - | 'snapshotLink' - | 'snapshotList' - | 'snapshotListEmpty' - | 'snapshotList.cell' - | 'snapshotList.closeButton' - | 'snapshotList.content' - | 'snapshotList.detailTitle' - | 'snapshotList.duration' - | 'snapshotList.endTime' - | 'snapshotList.euiFlyoutCloseButton' - | 'snapshotList.includeGlobalState' - | 'snapshotList.indices' - | 'snapshotList.reloadButton' - | 'snapshotList.repositoryLink' - | 'snapshotList.row' - | 'snapshotList.snapshotDetail' - | 'snapshotList.snapshotLink' - | 'snapshotList.snapshotTable' - | 'snapshotList.startTime' - | 'snapshotList.state' - | 'snapshotList.tab' - | 'snapshotList.tableHeaderCell_durationInMillis_3' - | 'snapshotList.tableHeaderCell_indices_4' - | 'snapshotList.tableHeaderCell_repository_1' - | 'snapshotList.tableHeaderCell_snapshot_0' - | 'snapshotList.tableHeaderCell_startTimeInMillis_2' - | 'snapshotList.tableHeaderSortButton' - | 'snapshotList.title' - | 'snapshotList.uuid' - | 'snapshotList.value' - | 'snapshotList.version' - | 'snapshotRestoreApp' - | 'snapshotRestoreApp.appTitle' - | 'snapshotRestoreApp.cell' - | 'snapshotRestoreApp.checkboxSelectAll' - | 'snapshotRestoreApp.checkboxSelectRow-my-repo' - | 'snapshotRestoreApp.closeButton' - | 'snapshotRestoreApp.content' - | 'snapshotRestoreApp.deleteRepositoryButton' - | 'snapshotRestoreApp.detailTitle' - | 'snapshotRestoreApp.documentationLink' - | 'snapshotRestoreApp.duration' - | 'snapshotRestoreApp.editRepositoryButton' - | 'snapshotRestoreApp.endTime' - | 'snapshotRestoreApp.euiFlyoutCloseButton' - | 'snapshotRestoreApp.includeGlobalState' - | 'snapshotRestoreApp.indices' - | 'snapshotRestoreApp.registerRepositoryButton' - | 'snapshotRestoreApp.reloadButton' - | 'snapshotRestoreApp.repositoryDetail' - | 'snapshotRestoreApp.repositoryLink' - | 'snapshotRestoreApp.repositoryList' - | 'snapshotRestoreApp.repositoryTable' - | 'snapshotRestoreApp.repositoryType' - | 'snapshotRestoreApp.row' - | 'snapshotRestoreApp.snapshotCount' - | 'snapshotRestoreApp.snapshotDetail' - | 'snapshotRestoreApp.snapshotLink' - | 'snapshotRestoreApp.snapshotList' - | 'snapshotRestoreApp.snapshotTable' - | 'snapshotRestoreApp.srRepositoryDetailsDeleteActionButton' - | 'snapshotRestoreApp.srRepositoryDetailsFlyoutCloseButton' - | 'snapshotRestoreApp.startTime' - | 'snapshotRestoreApp.state' - | 'snapshotRestoreApp.tab' - | 'snapshotRestoreApp.tableHeaderCell_durationInMillis_3' - | 'snapshotRestoreApp.tableHeaderCell_indices_4' - | 'snapshotRestoreApp.tableHeaderCell_name_0' - | 'snapshotRestoreApp.tableHeaderCell_repository_1' - | 'snapshotRestoreApp.tableHeaderCell_snapshot_0' - | 'snapshotRestoreApp.tableHeaderCell_startTimeInMillis_2' - | 'snapshotRestoreApp.tableHeaderCell_type_1' - | 'snapshotRestoreApp.tableHeaderSortButton' - | 'snapshotRestoreApp.title' - | 'snapshotRestoreApp.uuid' - | 'snapshotRestoreApp.value' - | 'snapshotRestoreApp.verifyRepositoryButton' - | 'snapshotRestoreApp.version' - | 'snapshotTable' - | 'snapshotTable.cell' - | 'snapshotTable.repositoryLink' - | 'snapshotTable.row' - | 'snapshotTable.snapshotLink' - | 'snapshotTable.tableHeaderCell_durationInMillis_3' - | 'snapshotTable.tableHeaderCell_indices_4' - | 'snapshotTable.tableHeaderCell_repository_1' - | 'snapshotTable.tableHeaderCell_snapshot_0' - | 'snapshotTable.tableHeaderCell_startTimeInMillis_2' - | 'snapshotTable.tableHeaderSortButton' - | 'srRepositoryDetailsDeleteActionButton' - | 'srRepositoryDetailsFlyoutCloseButton' - | 'startTime' - | 'startTime.title' - | 'startTime.value' - | 'state' - | 'state.title' - | 'state.value' - | 'repositories_tab' - | 'snapshots_tab' - | 'policies_tab' - | 'restore_status_tab' - | 'tableHeaderCell_durationInMillis_3' - | 'tableHeaderCell_durationInMillis_3.tableHeaderSortButton' - | 'tableHeaderCell_indices_4' - | 'tableHeaderCell_indices_4.tableHeaderSortButton' - | 'tableHeaderCell_name_0' - | 'tableHeaderCell_name_0.tableHeaderSortButton' - | 'tableHeaderCell_repository_1' - | 'tableHeaderCell_repository_1.tableHeaderSortButton' - | 'tableHeaderCell_shards.failed_6' - | 'tableHeaderCell_shards.total_5' - | 'tableHeaderCell_snapshot_0' - | 'tableHeaderCell_snapshot_0.tableHeaderSortButton' - | 'tableHeaderCell_startTimeInMillis_2' - | 'tableHeaderCell_startTimeInMillis_2.tableHeaderSortButton' - | 'tableHeaderCell_type_1' - | 'tableHeaderCell_type_1.tableHeaderSortButton' - | 'tableHeaderSortButton' - | 'title' - | 'uuid' - | 'uuid.title' - | 'uuid.value' - | 'value' - | 'verifyRepositoryButton' - | 'version' - | 'version.title' - | 'version.value' - | 'maxSnapshotsWarning' - | 'repositoryErrorsWarning' - | 'repositoryErrorsPrompt'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts index 605265f7311ba..662c50a98bfe8 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts @@ -92,11 +92,12 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const setCleanupRepositoryResponse = (response?: HttpResponse, error?: any) => { const status = error ? error.status || 503 : 200; + const body = error ? JSON.stringify(error) : JSON.stringify(response); server.respondWith('POST', `${API_BASE_PATH}repositories/:name/cleanup`, [ status, { 'Content-Type': 'application/json' }, - JSON.stringify(response), + body, ]); }; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 071868e23f7fe..7338e84f5c095 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -372,6 +372,60 @@ describe('', () => { expect(latestRequest.method).toBe('GET'); expect(latestRequest.url).toBe(`${API_BASE_PATH}repositories/${repo1.name}/verify`); }); + + describe('clean repository', () => { + test('shows results when request succeeds', async () => { + httpRequestsMockHelpers.setCleanupRepositoryResponse({ + cleanup: { + cleaned: true, + response: { + results: { + deleted_bytes: 0, + deleted_blobs: 0, + }, + }, + }, + }); + + const { exists, find, component } = testBed; + await act(async () => { + find('repositoryDetail.cleanupRepositoryButton').simulate('click'); + }); + component.update(); + + const latestRequest = server.requests[server.requests.length - 1]; + expect(latestRequest.method).toBe('POST'); + expect(latestRequest.url).toBe(`${API_BASE_PATH}repositories/${repo1.name}/cleanup`); + + expect(exists('repositoryDetail.cleanupCodeBlock')).toBe(true); + expect(exists('repositoryDetail.cleanupError')).toBe(false); + }); + + test('shows error when success fails', async () => { + httpRequestsMockHelpers.setCleanupRepositoryResponse({ + cleanup: { + cleaned: false, + error: { + message: 'Error message', + statusCode: 400, + }, + }, + }); + + const { exists, find, component } = testBed; + await act(async () => { + find('repositoryDetail.cleanupRepositoryButton').simulate('click'); + }); + component.update(); + + const latestRequest = server.requests[server.requests.length - 1]; + expect(latestRequest.method).toBe('POST'); + expect(latestRequest.url).toBe(`${API_BASE_PATH}repositories/${repo1.name}/cleanup`); + + expect(exists('repositoryDetail.cleanupCodeBlock')).toBe(false); + expect(exists('repositoryDetail.cleanupError')).toBe(true); + }); + }); }); describe('when the repository has been fetched (and has snapshots)', () => { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx index 91d01d44e093d..41054fe3e6033 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx @@ -356,21 +356,16 @@ export const RepositoryDetails: React.FunctionComponent = ({
) : ( - -

- {cleanup.error - ? JSON.stringify(cleanup.error) - : i18n.translate('xpack.snapshotRestore.repositoryDetails.cleanupUnknownError', { - defaultMessage: '503: Unknown error', - })} -

-
+ + } + error={cleanup.error as Error} + data-test-subj="cleanupError" + /> )} ) : null} diff --git a/x-pack/plugins/snapshot_restore/server/lib/wrap_es_error.ts b/x-pack/plugins/snapshot_restore/server/lib/wrap_es_error.ts index 6151aa9ef4a95..750e53222be03 100644 --- a/x-pack/plugins/snapshot_restore/server/lib/wrap_es_error.ts +++ b/x-pack/plugins/snapshot_restore/server/lib/wrap_es_error.ts @@ -34,7 +34,7 @@ export const wrapEsError = (err: any, statusCodeToMessageMap: any = {}) => { root_cause = [], // eslint-disable-line @typescript-eslint/naming-convention caused_by = {}, // eslint-disable-line @typescript-eslint/naming-convention } = {}, - } = JSON.parse(response); + } = typeof response === 'string' ? JSON.parse(response) : response; // If no custom message if specified for the error's status code, just // wrap the error as a Boom error response, include the additional information from ES, and return it diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts index c220d92280822..e700c6bf9e04e 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -233,12 +233,21 @@ export function registerRepositoriesRoutes({ try { const { body: cleanupResults } = await clusterClient.asCurrentUser.snapshot .cleanupRepository({ name }) - .catch((e) => ({ - body: { - cleaned: false, - error: e.response ? JSON.parse(e.response) : e, - }, - })); + .catch((e) => { + // This API returns errors in a non-standard format, which we'll need to + // munge to be compatible with wrapEsError. + const normalizedError = { + statusCode: e.meta.body.status, + response: e.meta.body, + }; + + return { + body: { + cleaned: false, + error: wrapEsError(normalizedError), + }, + }; + }); return res.ok({ body: { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a7b734ba20662..8bc0cef3a44e5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23213,7 +23213,6 @@ "xpack.snapshotRestore.repositoryDetails.cleanupErrorTitle": "申し訳ありません。リポジトリのクリーンアップ中にエラーが発生しました。", "xpack.snapshotRestore.repositoryDetails.cleanupRepositoryMessage": "スナップショットから参照されていないデータを削除するには、リポジトリをクリーンアップすることができます。これにより、ストレージ領域を解放できる場合があります。注:定期的にスナップショットを削除する場合は、この機能の利点が得られない可能性が高いため、使用頻度を低くしてください。", "xpack.snapshotRestore.repositoryDetails.cleanupTitle": "リポジトリのクリーンアップ", - "xpack.snapshotRestore.repositoryDetails.cleanupUnknownError": "503:不明なエラー", "xpack.snapshotRestore.repositoryDetails.closeButtonLabel": "閉じる", "xpack.snapshotRestore.repositoryDetails.editButtonLabel": "編集", "xpack.snapshotRestore.repositoryDetails.genericSettingsDescription": "レポジトリ「{name}」のランダムな設定です", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f5b364b35fa13..d3de9dab0df52 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23602,7 +23602,6 @@ "xpack.snapshotRestore.repositoryDetails.cleanupErrorTitle": "抱歉,清理存储库时出错。", "xpack.snapshotRestore.repositoryDetails.cleanupRepositoryMessage": "您可以清理存储库,以从快照中删除任何未引用的数据。这可节省存储空间。注意:如果定时删除快照,此功能可能并不那么有用,不应频繁使用。", "xpack.snapshotRestore.repositoryDetails.cleanupTitle": "存储库清理", - "xpack.snapshotRestore.repositoryDetails.cleanupUnknownError": "503:未知错误", "xpack.snapshotRestore.repositoryDetails.closeButtonLabel": "关闭", "xpack.snapshotRestore.repositoryDetails.editButtonLabel": "编辑", "xpack.snapshotRestore.repositoryDetails.genericSettingsDescription": "存储库“{name}”的只读设置", From 32f75a47ab3ae86f4abb2e8df640088c3f1e2835 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 9 Nov 2021 09:24:06 -0700 Subject: [PATCH 45/98] [SecuritySolution][Detections] Fixes rule status migration when alertId is not a string (#117962) ## Summary Resolves https://github.com/elastic/kibana/issues/116423, and adds an e2e test catching this behavior as we can't test via the migration test harness since the `siem-detection-engine-rule-status` SO isn't exposed within the SO Manager UI. Also adds note with regards to changes necessary once core issue https://github.com/elastic/kibana/issues/115153 is resolved. See https://github.com/elastic/kibana/pull/114585/files#r729620927. Note: existing `find_statuses`/`find_rules` integration tests will fail once fixed, so no additional tests necessary. ### Checklist Delete any items that are not applicable to this PR. - [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 --- .../lib/detection_engine/routes/utils.test.ts | 1 - .../rule_status_saved_objects_client.ts | 3 ++ .../legacy_rule_status/legacy_migrations.ts | 8 +++-- .../security_and_spaces/tests/migrations.ts | 31 +++++++++++++++++++ .../security_solution/migrations/data.json | 31 +++++++++++++++++++ 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index 6ddeeaa5ea1c2..66ad07b9d1029 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -136,7 +136,6 @@ describe.each([ describe('mergeStatuses', () => { it('merges statuses and converts from camelCase saved object to snake_case HTTP response', () => { - // const statusOne = exampleRuleStatus(); statusOne.attributes.status = RuleExecutionStatus.failed; const statusTwo = exampleRuleStatus(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts index 0026bba24eebe..cd26ab82b494a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/rule_status_saved_objects_client.ts @@ -67,6 +67,9 @@ export const ruleStatusSavedObjectsClientFactory = ( type: 'alert', })); const order: 'desc' = 'desc'; + // NOTE: Once https://github.com/elastic/kibana/issues/115153 is resolved + // ${legacyRuleStatusSavedObjectType}.statusDate will need to be updated to + // ${legacyRuleStatusSavedObjectType}.attributes.statusDate const aggs = { references: { nested: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts index 72ab4a2237ba1..877d693e5553c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts @@ -50,9 +50,13 @@ export const legacyMigrateRuleAlertIdSOReferences = ( const { alertId, ...otherAttributes } = doc.attributes; const existingReferences = doc.references ?? []; - // early return if alertId is not a string as expected + // early return if alertId is not a string as expected, still removing alertId as the mapping no longer exists if (!isString(alertId)) { - return { ...doc, references: existingReferences }; + return { + ...doc, + attributes: otherAttributes, + references: existingReferences, + }; } const alertReferences = legacyMigrateAlertId({ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts index d4eaf0d3dbf80..66d33de2ba4da 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts @@ -154,6 +154,37 @@ export default ({ getService }: FtrProviderContext): void => { lastLookBackDate: null, }); }); + + // If alertId is not a string/null ensure it is removed as the mapping was removed and so the migration would fail. + // Specific test for this as e2e tests will not catch the mismatch and we can't use the migration test harness + // since this SO is not importable/exportable via the SOM. + // For details see: https://github.com/elastic/kibana/issues/116423 + it('migrates legacy siem-detection-engine-rule-status and removes alertId when not a string', async () => { + const response = await es.get<{ + 'siem-detection-engine-rule-status': IRuleStatusSOAttributes; + }>( + { + index: '.kibana', + id: 'siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb36', + }, + { meta: true } + ); + expect(response.statusCode).to.eql(200); + + expect(response.body._source?.['siem-detection-engine-rule-status']).to.eql({ + statusDate: '2021-10-11T20:51:26.622Z', + status: 'succeeded', + lastFailureAt: '2021-10-11T18:10:08.982Z', + lastSuccessAt: '2021-10-11T20:51:26.622Z', + lastFailureMessage: + '4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: "Threshy" id: "fb1046a0-0452-11ec-9b15-d13d79d162f3" rule id: "b789c80f-f6d8-41f1-8b4f-b4a23342cde2" signals index: ".siem-signals-spong-default"', + lastSuccessMessage: 'succeeded', + gap: '4 days', + bulkCreateTimeDurations: ['34.49'], + searchAfterTimeDurations: ['62.58'], + lastLookBackDate: null, + }); + }); }); }); }; diff --git a/x-pack/test/functional/es_archives/security_solution/migrations/data.json b/x-pack/test/functional/es_archives/security_solution/migrations/data.json index 97a2596f9dba1..89eb207b392e8 100644 --- a/x-pack/test/functional/es_archives/security_solution/migrations/data.json +++ b/x-pack/test/functional/es_archives/security_solution/migrations/data.json @@ -61,3 +61,34 @@ } } +{ + "type": "doc", + "value": { + "id": "siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb36", + "index": ".kibana_1", + "source": { + "siem-detection-engine-rule-status": { + "alertId": 1337, + "statusDate": "2021-10-11T20:51:26.622Z", + "status": "succeeded", + "lastFailureAt": "2021-10-11T18:10:08.982Z", + "lastSuccessAt": "2021-10-11T20:51:26.622Z", + "lastFailureMessage": "4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: \"Threshy\" id: \"fb1046a0-0452-11ec-9b15-d13d79d162f3\" rule id: \"b789c80f-f6d8-41f1-8b4f-b4a23342cde2\" signals index: \".siem-signals-spong-default\"", + "lastSuccessMessage": "succeeded", + "gap": "4 days", + "bulkCreateTimeDurations": [ + "34.49" + ], + "searchAfterTimeDurations": [ + "62.58" + ], + "lastLookBackDate": null + }, + "type": "siem-detection-engine-rule-status", + "references": [], + "coreMigrationVersion": "7.14.0", + "updated_at": "2021-10-11T20:51:26.657Z" + } + } +} + From 56bfd0e53dc386a0835a801b7557db1c87aa036b Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 9 Nov 2021 11:43:09 -0500 Subject: [PATCH 46/98] [Dashboard] [Functional Tests] Unskip by value Example (#116513) * Unskip dashboard by value example tests --- .../public/hello_world/hello_world_embeddable.tsx | 3 ++- .../embeddable_examples/public/todo/todo_component.tsx | 2 +- .../embeddable_examples/public/todo/todo_ref_component.tsx | 2 +- test/examples/embeddables/dashboard.ts | 7 ++++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/embeddable_examples/public/hello_world/hello_world_embeddable.tsx b/examples/embeddable_examples/public/hello_world/hello_world_embeddable.tsx index 3d0e82b6f99c1..afec509037434 100644 --- a/examples/embeddable_examples/public/hello_world/hello_world_embeddable.tsx +++ b/examples/embeddable_examples/public/hello_world/hello_world_embeddable.tsx @@ -33,7 +33,8 @@ export class HelloWorldEmbeddable extends Embeddable { * @param node */ public render(node: HTMLElement) { - node.innerHTML = '
HELLO WORLD!
'; + node.innerHTML = + '
HELLO WORLD!
'; } /** diff --git a/examples/embeddable_examples/public/todo/todo_component.tsx b/examples/embeddable_examples/public/todo/todo_component.tsx index 10d48881c72e5..f2ba44de407ee 100644 --- a/examples/embeddable_examples/public/todo/todo_component.tsx +++ b/examples/embeddable_examples/public/todo/todo_component.tsx @@ -41,7 +41,7 @@ function wrapSearchTerms(task: string, search?: string) { export function TodoEmbeddableComponentInner({ input: { icon, title, task, search } }: Props) { return ( - + {icon ? : } diff --git a/examples/embeddable_examples/public/todo/todo_ref_component.tsx b/examples/embeddable_examples/public/todo/todo_ref_component.tsx index 53ed20042dc5b..d70db903d1dac 100644 --- a/examples/embeddable_examples/public/todo/todo_ref_component.tsx +++ b/examples/embeddable_examples/public/todo/todo_ref_component.tsx @@ -45,7 +45,7 @@ export function TodoRefEmbeddableComponentInner({ const title = savedAttributes?.title; const task = savedAttributes?.task; return ( - + {icon ? ( diff --git a/test/examples/embeddables/dashboard.ts b/test/examples/embeddables/dashboard.ts index 5c255b136c666..23f221c40d4da 100644 --- a/test/examples/embeddables/dashboard.ts +++ b/test/examples/embeddables/dashboard.ts @@ -97,11 +97,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const pieChart = getService('pieChart'); const dashboardExpect = getService('dashboardExpect'); const elasticChart = getService('elasticChart'); - const PageObjects = getPageObjects(['common', 'visChart']); + const PageObjects = getPageObjects(['common', 'visChart', 'dashboard']); const monacoEditor = getService('monacoEditor'); - // FLAKY: https://github.com/elastic/kibana/issues/116414 - describe.skip('dashboard container', () => { + describe('dashboard container', () => { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); await esArchiver.loadIfNeeded( @@ -109,6 +108,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide ); await PageObjects.common.navigateToApp('dashboardEmbeddableExamples'); await testSubjects.click('dashboardEmbeddableByValue'); + await PageObjects.dashboard.waitForRenderComplete(); + await updateInput(JSON.stringify(testDashboardInput, null, 4)); }); From c44dd6e7010fa0085ddc6d6a0bf25a5c0a90bad3 Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Tue, 9 Nov 2021 11:58:20 -0500 Subject: [PATCH 47/98] [Security Solution][RAC] Threshold Rule Fixes (#117571) * Remove unused code * Fix threshold field refs * Fix import * UI fixes for Rules * Threshold Cypress test fixes * Type fixes * Threshold signal test fixes * Handle legacy schema optionally * Fix threshold integration test * More test fixes --- .../detection_rules/threshold_rule.spec.ts | 6 +-- .../cases/components/case_view/index.tsx | 1 - .../components/alerts_table/actions.test.tsx | 8 ---- .../components/alerts_table/actions.tsx | 43 +++---------------- .../investigate_in_timeline_action.tsx | 4 -- .../use_investigate_in_timeline.tsx | 4 -- .../components/alerts_table/types.ts | 2 - .../detection_engine/rules/types.ts | 1 + .../rules/use_rule_with_fallback.tsx | 14 ++++-- .../timeline/body/actions/index.tsx | 1 - .../rule_types/factories/utils/build_alert.ts | 10 ++--- .../factories/utils/filter_source.ts | 6 ++- .../tests/generating_signals.ts | 7 +-- .../tests/keyword_family/const_keyword.ts | 3 +- .../tests/keyword_family/keyword.ts | 3 +- .../keyword_mixed_with_const.ts | 3 +- 16 files changed, 40 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 4c76fdcb18ca7..02d8837261f2f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -99,7 +99,7 @@ describe('Detection rules, threshold', () => { waitForAlertsIndexToBeCreated(); }); - it.skip('Creates and activates a new threshold rule', () => { + it('Creates and activates a new threshold rule', () => { goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); goToCreateNewRule(); @@ -171,9 +171,7 @@ describe('Detection rules, threshold', () => { waitForAlertsToPopulate(); cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text().split(' ')[0]).to.be.lt(100)); - cy.get(ALERT_GRID_CELL).eq(3).contains(rule.name); - cy.get(ALERT_GRID_CELL).eq(4).contains(rule.severity.toLowerCase()); - cy.get(ALERT_GRID_CELL).eq(5).contains(rule.riskScore); + cy.get(ALERT_GRID_CELL).contains(rule.name); }); it('Preview results of keyword using "host.name"', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index fc65d5c370bae..06fce07399b6e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -75,7 +75,6 @@ const InvestigateInTimelineActionComponent = (alertIds: string[]) => { alertIds={alertIds} key="investigate-in-timeline" ecsRowData={null} - nonEcsRowData={[]} /> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index a4660356b8630..d37ba65eb8a89 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -76,7 +76,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: mockEcsDataWithAlert, - nonEcsData: [], updateTimelineIsLoading, searchStrategyClient, }); @@ -92,7 +91,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: mockEcsDataWithAlert, - nonEcsData: [], updateTimelineIsLoading, searchStrategyClient, }); @@ -249,7 +247,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: mockEcsDataWithAlert, - nonEcsData: [], updateTimelineIsLoading, searchStrategyClient, }); @@ -267,7 +264,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: mockEcsDataWithAlert, - nonEcsData: [], updateTimelineIsLoading, searchStrategyClient, }); @@ -301,7 +297,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, - nonEcsData: [], updateTimelineIsLoading, searchStrategyClient, }); @@ -327,7 +322,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, - nonEcsData: [], updateTimelineIsLoading, searchStrategyClient, }); @@ -357,7 +351,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, - nonEcsData: [], updateTimelineIsLoading, searchStrategyClient, }); @@ -398,7 +391,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, - nonEcsData: [], updateTimelineIsLoading, searchStrategyClient, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 213f8c78e3b8d..aec7f255ad588 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -7,7 +7,7 @@ /* eslint-disable complexity */ -import { get, getOr, isEmpty } from 'lodash/fp'; +import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; import dateMath from '@elastic/datemath'; @@ -37,7 +37,6 @@ import { } from './types'; import { Ecs } from '../../../../common/ecs'; import { - TimelineNonEcsData, TimelineEventsDetailsItem, TimelineEventsDetailsRequestOptions, TimelineEventsDetailsStrategyResponse, @@ -75,26 +74,6 @@ export const getUpdateAlertsQuery = (eventIds: Readonly) => { }; }; -export const getFilterAndRuleBounds = ( - data: TimelineNonEcsData[][] -): [string[], number, number] => { - const stringFilter = - data?.[0].filter( - (d) => d.field === 'signal.rule.filters' || d.field === 'kibana.alert.rule.filters' - )?.[0]?.value ?? []; - - const eventTimes = data - .flatMap( - (alert) => - alert.filter( - (d) => d.field === 'signal.original_time' || d.field === 'kibana.alert.original_time' - )?.[0]?.value ?? [] - ) - .map((d) => moment(d)); - - return [stringFilter, moment.min(eventTimes).valueOf(), moment.max(eventTimes).valueOf()]; -}; - export const updateAlertStatusAction = async ({ query, alertIds, @@ -174,11 +153,7 @@ const getFiltersFromRule = (filters: string[]): Filter[] => } }, [] as Filter[]); -export const getThresholdAggregationData = ( - ecsData: Ecs | Ecs[], - nonEcsData: TimelineNonEcsData[] -): ThresholdAggregationData => { - // TODO: AAD fields +export const getThresholdAggregationData = (ecsData: Ecs | Ecs[]): ThresholdAggregationData => { const thresholdEcsData: Ecs[] = Array.isArray(ecsData) ? ecsData : [ecsData]; return thresholdEcsData.reduce( (outerAcc, thresholdData) => { @@ -195,11 +170,9 @@ export const getThresholdAggregationData = ( }; try { - try { - thresholdResult = JSON.parse((thresholdData.signal?.threshold_result as string[])[0]); - } catch (err) { - thresholdResult = JSON.parse((get(ALERT_THRESHOLD_RESULT, thresholdData) as string[])[0]); - } + thresholdResult = JSON.parse( + (getField(thresholdData, ALERT_THRESHOLD_RESULT) as string[])[0] + ); aggField = JSON.parse(threshold[0]).field; } catch (err) { // Legacy support @@ -401,7 +374,6 @@ export const buildEqlDataProviderOrFilter = ( export const sendAlertToTimelineAction = async ({ createTimeline, ecsData: ecs, - nonEcsData, updateTimelineIsLoading, searchStrategyClient, }: SendAlertToTimelineActionProps) => { @@ -498,10 +470,7 @@ export const sendAlertToTimelineAction = async ({ } if (isThresholdRule(ecsData)) { - const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData( - ecsData, - nonEcsData - ); + const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(ecsData); return createTimeline({ from: thresholdFrom, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx index 04cba8332553a..bca04dcf37a5b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { Ecs } from '../../../../../common/ecs'; -import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; import { ActionIconItem } from '../../../../timelines/components/timeline/body/actions/action_icon_item'; import { @@ -19,7 +18,6 @@ import { useInvestigateInTimeline } from './use_investigate_in_timeline'; interface InvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; - nonEcsRowData?: TimelineNonEcsData[]; ariaLabel?: string; alertIds?: string[]; buttonType?: 'text' | 'icon'; @@ -30,13 +28,11 @@ const InvestigateInTimelineActionComponent: React.FC { const { investigateInTimelineAlertClick } = useInvestigateInTimeline({ ecsRowData, - nonEcsRowData, alertIds, onInvestigateInTimelineAlertClick, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index ca296067336cc..c6bd5d9aa05bc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -30,7 +30,6 @@ interface UseInvestigateInTimelineActionProps { export const useInvestigateInTimeline = ({ ecsRowData, - nonEcsRowData, alertIds, onInvestigateInTimelineAlertClick, }: UseInvestigateInTimelineActionProps) => { @@ -90,7 +89,6 @@ export const useInvestigateInTimeline = ({ await sendAlertToTimelineAction({ createTimeline, ecsData: alertsEcsData, - nonEcsData: nonEcsRowData ?? [], searchStrategyClient, updateTimelineIsLoading, }); @@ -100,7 +98,6 @@ export const useInvestigateInTimeline = ({ await sendAlertToTimelineAction({ createTimeline, ecsData: ecsRowData, - nonEcsData: nonEcsRowData ?? [], searchStrategyClient, updateTimelineIsLoading, }); @@ -109,7 +106,6 @@ export const useInvestigateInTimeline = ({ alertsEcsData, createTimeline, ecsRowData, - nonEcsRowData, onInvestigateInTimelineAlertClick, searchStrategyClient, updateTimelineIsLoading, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index 290f9ed560f2e..3e525bfe25ad9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -8,7 +8,6 @@ import { ISearchStart } from '../../../../../../../src/plugins/data/public'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Ecs } from '../../../../common/ecs'; -import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { NoteResult } from '../../../../common/types/timeline/note'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { TimelineModel } from '../../../timelines/store/timeline/model'; @@ -54,7 +53,6 @@ export interface UpdateAlertStatusActionProps { export interface SendAlertToTimelineActionProps { createTimeline: CreateTimeline; ecsData: Ecs | Ecs[]; - nonEcsData: TimelineNonEcsData[]; updateTimelineIsLoading: UpdateTimelineLoading; searchStrategyClient: ISearchStart; } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index bd287b4647048..fea28eba61d70 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -145,6 +145,7 @@ export const RuleSchema = t.intersection([ timestamp_override, note: t.string, exceptions_list: listArray, + uuid: t.string, version: t.number, }), ]); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx index da56275280f65..7f1c70576d870 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx @@ -28,8 +28,13 @@ interface AlertHit { _index: string; _source: { '@timestamp': string; - signal: { - rule: Rule; + signal?: { + rule?: Rule; + }; + kibana?: { + alert?: { + rule?: Rule; + }; }; }; } @@ -77,7 +82,10 @@ export const useRuleWithFallback = (ruleId: string): UseRuleWithFallback => { }, [addError, error]); const rule = useMemo(() => { - const result = isExistingRule ? ruleData : alertsData?.hits.hits[0]?._source.signal.rule; + const hit = alertsData?.hits.hits[0]; + const result = isExistingRule + ? ruleData + : hit?._source.signal?.rule ?? hit?._source.kibana?.alert?.rule; if (result) { return transformInput(result); } 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 492b256cd7659..27af395f11dd2 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 @@ -169,7 +169,6 @@ const ActionsComponent: React.FC = ({ ariaLabel={i18n.SEND_ALERT_TO_TIMELINE_FOR_ROW({ ariaRowindex, columnValues })} key="investigate-in-timeline" ecsRowData={ecsData} - nonEcsRowData={data} /> )} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index bf07d2bb8515f..612d36d8ad8f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -35,6 +35,7 @@ import { ALERT_ANCESTORS, ALERT_DEPTH, ALERT_ORIGINAL_TIME, + ALERT_THRESHOLD_RESULT, ALERT_ORIGINAL_EVENT, } from '../../../../../../common/field_maps/field_names'; @@ -59,10 +60,10 @@ export const buildParent = (doc: SimpleHit): Ancestor => { id: doc._id, type: isSignal ? 'signal' : 'event', index: doc._index, - depth: isSignal ? getField(doc, 'signal.depth') ?? 1 : 0, + depth: isSignal ? getField(doc, ALERT_DEPTH) ?? 1 : 0, }; if (isSignal) { - parent.rule = getField(doc, 'signal.rule.id'); + parent.rule = getField(doc, ALERT_RULE_UUID); } return parent; }; @@ -73,9 +74,8 @@ export const buildParent = (doc: SimpleHit): Ancestor => { * @param doc The parent event for which to extend the ancestry. */ export const buildAncestors = (doc: SimpleHit): Ancestor[] => { - // TODO: handle alerts-on-legacy-alerts const newAncestor = buildParent(doc); - const existingAncestors: Ancestor[] = getField(doc, 'signal.ancestors') ?? []; + const existingAncestors: Ancestor[] = getField(doc, ALERT_ANCESTORS) ?? []; return [...existingAncestors, newAncestor]; }; @@ -130,7 +130,7 @@ export const additionalAlertFields = (doc: BaseSignalHit) => { }); const additionalFields: Record = { [ALERT_ORIGINAL_TIME]: originalTime != null ? originalTime.toISOString() : undefined, - ...(thresholdResult != null ? { threshold_result: thresholdResult } : {}), + ...(thresholdResult != null ? { [ALERT_THRESHOLD_RESULT]: thresholdResult } : {}), }; for (const [key, val] of Object.entries(doc._source ?? {})) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts index ead72bdd6fd8b..3493025749f98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ALERT_THRESHOLD_RESULT } from '../../../../../../common/field_maps/field_names'; import { SignalSourceHit } from '../../../signals/types'; import { RACAlert } from '../../types'; @@ -12,10 +13,13 @@ export const filterSource = (doc: SignalSourceHit): Partial => { const docSource = doc._source ?? {}; const { event, - threshold_result: thresholdResult, + threshold_result: siemSignalsThresholdResult, + [ALERT_THRESHOLD_RESULT]: alertThresholdResult, ...filteredSource } = docSource || { + event: null, threshold_result: null, + [ALERT_THRESHOLD_RESULT]: null, }; return filteredSource; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 81291b3d46118..b28ff3fdc714d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -54,6 +54,7 @@ import { ALERT_ORIGINAL_EVENT, ALERT_ORIGINAL_EVENT_CATEGORY, ALERT_GROUP_ID, + ALERT_THRESHOLD_RESULT, } from '../../../../plugins/security_solution/common/field_maps/field_names'; /** @@ -728,7 +729,7 @@ export default ({ getService }: FtrProviderContext) => { [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], [ALERT_DEPTH]: 1, - threshold_result: { + [ALERT_THRESHOLD_RESULT]: { terms: [ { field: 'host.id', @@ -849,7 +850,7 @@ export default ({ getService }: FtrProviderContext) => { [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], [ALERT_DEPTH]: 1, - threshold_result: { + [ALERT_THRESHOLD_RESULT]: { terms: [ { field: 'host.id', @@ -916,7 +917,7 @@ export default ({ getService }: FtrProviderContext) => { [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], [ALERT_DEPTH]: 1, - threshold_result: { + [ALERT_THRESHOLD_RESULT]: { terms: [ { field: 'event.module', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/const_keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/const_keyword.ts index 80e85fb491fc1..17492248f537a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/const_keyword.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/const_keyword.ts @@ -10,6 +10,7 @@ import { EqlCreateSchema, ThresholdCreateSchema, } from '../../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { ALERT_THRESHOLD_RESULT } from '../../../../../plugins/security_solution/common/field_maps/field_names'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -131,7 +132,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, log, 1, [id]); const signalsOpen = await getSignalsById(supertest, log, id); const hits = signalsOpen.hits.hits - .map((hit) => hit._source?.threshold_result ?? null) + .map((hit) => hit._source?.[ALERT_THRESHOLD_RESULT] ?? null) .sort(); expect(hits).to.eql([ { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword.ts index bdfe496bd3fa6..642b65f6a49c3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword.ts @@ -25,6 +25,7 @@ import { QueryCreateSchema, ThresholdCreateSchema, } from '../../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { ALERT_THRESHOLD_RESULT } from '../../../../../plugins/security_solution/common/field_maps/field_names'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -105,7 +106,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, log, 1, [id]); const signalsOpen = await getSignalsById(supertest, log, id); const hits = signalsOpen.hits.hits - .map((hit) => hit._source?.threshold_result ?? null) + .map((hit) => hit._source?.[ALERT_THRESHOLD_RESULT] ?? null) .sort(); expect(hits).to.eql([ { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword_mixed_with_const.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword_mixed_with_const.ts index 4076a34b6139b..df158b239c120 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword_mixed_with_const.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword_mixed_with_const.ts @@ -10,6 +10,7 @@ import { EqlCreateSchema, ThresholdCreateSchema, } from '../../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { ALERT_THRESHOLD_RESULT } from '../../../../../plugins/security_solution/common/field_maps/field_names'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -144,7 +145,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, log, 1, [id]); const signalsOpen = await getSignalsById(supertest, log, id); const hits = signalsOpen.hits.hits - .map((hit) => hit._source?.threshold_result ?? null) + .map((hit) => hit._source?.[ALERT_THRESHOLD_RESULT] ?? null) .sort(); expect(hits).to.eql([ { From f343870495bdcc7c35b18b5a4f2e98e1c186474b Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:14:58 -0500 Subject: [PATCH 48/98] [Security Solution][Endpoint][Policy] Automate policy tests (#117741) --- .../management/pages/endpoint_hosts/view/index.test.tsx | 5 +++++ .../view/policy_forms/components/supported_version.tsx | 2 +- .../apps/endpoint/policy_details.ts | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index c6d41fbdc4b1b..5db501f5e09ac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -1086,6 +1086,11 @@ describe('when on the endpoint list page', () => { ).toBe('Policy Response'); }); + it('should display timestamp', () => { + const timestamp = renderResult.queryByTestId('endpointDetailsPolicyResponseTimestamp'); + expect(timestamp).not.toBeNull(); + }); + it('should show a configuration section for each protection', async () => { const configAccordions = await renderResult.findAllByTestId( 'endpointDetailsPolicyResponseConfigAccordion' diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx index b8418004206b9..0d90819b9cd15 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx @@ -17,7 +17,7 @@ export const SupportedVersionNotice = ({ optionName }: { optionName: string }) = } return ( - + { + const supportedVersion = await testSubjects.find('policySupportedVersions'); + expect(supportedVersion).to.be('Agent version ' + popupVersionsMap.get('malware')); + }); + it('should show the custom message text area when the Notify User checkbox is checked', async () => { expect(await testSubjects.isChecked('malwareUserNotificationCheckbox')).to.be(true); await testSubjects.existOrFail('malwareUserNotificationCustomMessage'); From 687b97b7e40292b3954a5f61042a8ce6437d8b0c Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 9 Nov 2021 18:41:48 +0100 Subject: [PATCH 49/98] [heatmap] Fix x time scale with calendar intervals (#117619) The commit fixes the Lens heatmap time axis when used with calendars time intervals in date_histogram aggs --- package.json | 2 +- .../heatmap_visualization/chart_component.tsx | 35 +++++++++++++++++-- .../explorer/swimlane_container.tsx | 11 +++++- yarn.lock | 8 ++--- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 8c29c8bcf2ff6..587c3a31316fd 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "@elastic/apm-rum": "^5.9.1", "@elastic/apm-rum-react": "^1.3.1", "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", - "@elastic/charts": "38.1.3", + "@elastic/charts": "39.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.35", "@elastic/ems-client": "8.0.0", diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index c999656071ef4..5e4c2f2684062 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -16,6 +16,8 @@ import { HeatmapSpec, ScaleType, Settings, + ESFixedIntervalUnit, + ESCalendarIntervalUnit, } from '@elastic/charts'; import type { CustomPaletteState } from 'src/plugins/charts/public'; import { VisualizationContainer } from '../visualization_container'; @@ -30,6 +32,7 @@ import { } from '../shared_components'; import { LensIconChartHeatmap } from '../assets/chart_heatmap'; import { DEFAULT_PALETTE_NAME } from './constants'; +import { search } from '../../../../../src/plugins/data/public'; declare global { interface Window { @@ -162,8 +165,30 @@ export const HeatmapComponent: FC = ({ // Fallback to the ordinal scale type when a single row of data is provided. // Related issue https://github.com/elastic/elastic-charts/issues/1184 - const xScaleType = - isTimeBasedSwimLane && chartData.length > 1 ? ScaleType.Time : ScaleType.Ordinal; + + let xScale: HeatmapSpec['xScale'] = { type: ScaleType.Ordinal }; + if (isTimeBasedSwimLane && chartData.length > 1) { + const dateInterval = + search.aggs.getDateHistogramMetaDataByDatatableColumn(xAxisColumn)?.interval; + const esInterval = dateInterval ? search.aggs.parseEsInterval(dateInterval) : undefined; + if (esInterval) { + xScale = { + type: ScaleType.Time, + interval: + esInterval.type === 'fixed' + ? { + type: 'fixed', + unit: esInterval.unit as ESFixedIntervalUnit, + value: esInterval.value, + } + : { + type: 'calendar', + unit: esInterval.unit as ESCalendarIntervalUnit, + value: esInterval.value, + }, + }; + } + } const xValuesFormatter = formatFactory(xAxisMeta.params); const valueFormatter = formatFactory(valueColumn.meta.params); @@ -341,6 +366,10 @@ export const HeatmapComponent: FC = ({ labelOptions: { maxLines: args.legend.shouldTruncate ? args.legend?.maxLines ?? 1 : 0 }, }, }} + xDomain={{ + min: data.dateRange?.fromDate.getTime() ?? NaN, + max: data.dateRange?.toDate.getTime() ?? NaN, + }} onBrushEnd={onBrushEnd as BrushEndListener} /> = ({ yAccessor={args.yAccessor || 'unifiedY'} valueAccessor={args.valueAccessor} valueFormatter={(v: number) => valueFormatter.convert(v)} - xScaleType={xScaleType} + xScale={xScale} ySortPredicate="dataIndex" config={config} xSortPredicate="dataIndex" diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index ef8e80381293e..2cdb18666d0ee 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -470,7 +470,16 @@ export const SwimlaneContainer: FC = ({ valueAccessor="value" highlightedData={highlightedData} valueFormatter={getFormattedSeverityScore} - xScaleType={ScaleType.Time} + xScale={{ + type: ScaleType.Time, + interval: { + type: 'fixed', + unit: 'ms', + // the xDomain.minInterval should always be available at rendering time + // adding a fallback to 1m bucket + value: xDomain?.minInterval ?? 1000 * 60, + }, + }} ySortPredicate="dataIndex" config={swimLaneConfig} /> diff --git a/yarn.lock b/yarn.lock index 28c729ee8c94e..40b4d29f75ba3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2337,10 +2337,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@38.1.3": - version "38.1.3" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-38.1.3.tgz#0bf27021c54176e87d38269613205f3d6219da96" - integrity sha512-p6bJQWmfJ5SLkcgAoAMB3eTah/2a3/r3uo3ZskEN/xdPiqU8P+GANF8+6F4dWNfejbrpSUyCUldl7S4nWFGg3Q== +"@elastic/charts@39.0.0": + version "39.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-39.0.0.tgz#85e615f550d03d8fb880bf44e891452b4341706b" + integrity sha512-EnmOXFAN5u9rkcwM4L2AksxoWpOpZRXbjX2HYAxgj8WcBb14zYoYeyENMQyG/qu2Rm6PnUni0dgy+mPOTEnGmw== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From d1ebdf17de77e35bde214028a1199c6477999bde Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Tue, 9 Nov 2021 17:42:47 +0000 Subject: [PATCH 50/98] Update telemetry filter list for Security Solution (#117995) * Add failing test condition. * implement filter list condition. --- .../plugins/security_solution/server/lib/telemetry/filters.ts | 1 + .../security_solution/server/lib/telemetry/sender.test.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts index b3316458365d5..40377ba72547c 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts @@ -67,6 +67,7 @@ const allowlistBaseEventFields: AllowlistFields = { hash: true, Ext: { code_signature: true, + header_bytes: true, header_data: true, malware_classification: true, malware_signature: true, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 46ed0b1f0bfb6..70852aa3093c6 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -68,6 +68,7 @@ describe('TelemetryEventsSender', () => { malware_signature: { key1: 'X', }, + header_bytes: 'data in here', quarantine_result: true, quarantine_message: 'this file is bad', something_else: 'nope', @@ -132,6 +133,7 @@ describe('TelemetryEventsSender', () => { key1: 'X', key2: 'Y', }, + header_bytes: 'data in here', malware_classification: { key1: 'X', }, From 3789b4ba54c9c7205da3a35ebd1763b194fa8bbf Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 9 Nov 2021 19:01:11 +0100 Subject: [PATCH 51/98] unskip copy to space test (#118005) --- .../dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts index f42be7b4229f9..3e83d38593601 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -156,8 +156,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/83824 - describe.skip('Copy to space', () => { + describe('Copy to space', () => { const destinationSpaceId = 'custom_space'; before(async () => { await spaces.create({ From 2f171fbd0092773b739be26c01bf37f54b6ef9ec Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 9 Nov 2021 19:01:39 +0100 Subject: [PATCH 52/98] [Lens] Reference line: fix overlapping areas (#117722) * :bug: Fix bug with overlap * :white_check_mark: Add tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../expression_reference_lines.test.tsx | 387 ++++++++++++++++++ .../expression_reference_lines.tsx | 31 +- 2 files changed, 403 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx new file mode 100644 index 0000000000000..85d5dd362a431 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.test.tsx @@ -0,0 +1,387 @@ +/* + * 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 { LineAnnotation, RectAnnotation } from '@elastic/charts'; +import { shallow } from 'enzyme'; +import React from 'react'; +import { PaletteOutput } from 'src/plugins/charts/common'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { FieldFormat } from 'src/plugins/field_formats/common'; +import { layerTypes, LensMultiTable } from '../../common'; +import { LayerArgs, YConfig } from '../../common/expressions'; +import { + ReferenceLineAnnotations, + ReferenceLineAnnotationsProps, +} from './expression_reference_lines'; + +const paletteService = chartPluginMock.createPaletteRegistry(); + +const mockPaletteOutput: PaletteOutput = { + type: 'palette', + name: 'mock', + params: {}, +}; + +const row: Record = { + xAccessorFirstId: 1, + xAccessorSecondId: 2, + yAccessorLeftFirstId: 5, + yAccessorLeftSecondId: 10, + yAccessorRightFirstId: 5, + yAccessorRightSecondId: 10, +}; + +const histogramData: LensMultiTable = { + type: 'lens_multitable', + tables: { + firstLayer: { + type: 'datatable', + rows: [row], + columns: Object.keys(row).map((id) => ({ + id, + name: `Static value: ${row[id]}`, + meta: { + type: 'number', + params: { id: 'number' }, + }, + })), + }, + }, + dateRange: { + fromDate: new Date('2020-04-01T16:14:16.246Z'), + toDate: new Date('2020-04-01T17:15:41.263Z'), + }, +}; + +function createLayers(yConfigs: LayerArgs['yConfig']): LayerArgs[] { + return [ + { + layerId: 'firstLayer', + layerType: layerTypes.REFERENCE_LINE, + hide: false, + yScaleType: 'linear', + xScaleType: 'linear', + isHistogram: false, + seriesType: 'bar_stacked', + accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), + palette: mockPaletteOutput, + yConfig: yConfigs, + }, + ]; +} + +interface YCoords { + y0: number | undefined; + y1: number | undefined; +} +interface XCoords { + x0: number | undefined; + x1: number | undefined; +} + +function getAxisFromId(layerPrefix: string): YConfig['axisMode'] { + return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom'; +} + +const emptyCoords = { x0: undefined, x1: undefined, y0: undefined, y1: undefined }; + +describe('ReferenceLineAnnotations', () => { + describe('with fill', () => { + let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + let defaultProps: Omit; + + beforeEach(() => { + formatters = { + left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, + }; + + defaultProps = { + formatters, + paletteService, + syncColors: false, + isHorizontal: false, + axesMap: { left: true, right: false }, + paddingMap: {}, + }; + }); + + it.each([ + ['yAccessorLeft', 'above'], + ['yAccessorLeft', 'below'], + ['yAccessorRight', 'above'], + ['yAccessorRight', 'below'], + ] as Array<[string, YConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const axisMode = getAxisFromId(layerPrefix); + const wrapper = shallow( + + ); + + const y0 = fill === 'above' ? 5 : undefined; + const y1 = fill === 'above' ? undefined : 5; + + expect(wrapper.find(LineAnnotation).exists()).toBe(true); + expect(wrapper.find(RectAnnotation).exists()).toBe(true); + expect(wrapper.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { x0: undefined, x1: undefined, y0, y1 }, + details: y0 ?? y1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above'], + ['xAccessor', 'below'], + ] as Array<[string, YConfig['fill']]>)( + 'should render a RectAnnotation for a reference line with fill set: %s %s', + (layerPrefix, fill) => { + const wrapper = shallow( + + ); + + const x0 = fill === 'above' ? 1 : undefined; + const x1 = fill === 'above' ? undefined : 1; + + expect(wrapper.find(LineAnnotation).exists()).toBe(true); + expect(wrapper.find(RectAnnotation).exists()).toBe(true); + expect(wrapper.find(RectAnnotation).prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, x0, x1 }, + details: x0 ?? x1, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['yAccessorLeft', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ] as Array<[string, YConfig['fill'], YCoords, YCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const axisMode = getAxisFromId(layerPrefix); + const wrapper = shallow( + + ); + + expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.y0 ?? coordsA.y1, + header: undefined, + }, + ]) + ); + expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.y1 ?? coordsB.y0, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], + ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], + ] as Array<[string, YConfig['fill'], XCoords, XCoords]>)( + 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', + (layerPrefix, fill, coordsA, coordsB) => { + const wrapper = shallow( + + ); + + expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.x0 ?? coordsA.x1, + header: undefined, + }, + ]) + ); + expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.x1 ?? coordsB.x0, + header: undefined, + }, + ]) + ); + } + ); + + it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( + 'should let areas in different directions overlap: %s', + (layerPrefix) => { + const axisMode = getAxisFromId(layerPrefix); + + const wrapper = shallow( + + ); + + expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x0: 1 } : { y0: 5 }) }, + details: axisMode === 'bottom' ? 1 : 5, + header: undefined, + }, + ]) + ); + expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x1: 2 } : { y1: 10 }) }, + details: axisMode === 'bottom' ? 2 : 10, + header: undefined, + }, + ]) + ); + } + ); + + it.each([ + ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], + ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], + ] as Array<[YConfig['fill'], YCoords, YCoords]>)( + 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', + (fill, coordsA, coordsB) => { + const wrapper = shallow( + + ); + + expect(wrapper.find(RectAnnotation).first().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsA }, + details: coordsA.y0 ?? coordsA.y1, + header: undefined, + }, + ]) + ); + expect(wrapper.find(RectAnnotation).last().prop('dataValues')).toEqual( + expect.arrayContaining([ + { + coordinates: { ...emptyCoords, ...coordsB }, + details: coordsB.y1 ?? coordsB.y0, + header: undefined, + }, + ]) + ); + } + ); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx index 42e02871026df..d41baff0bc1dc 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx @@ -17,7 +17,7 @@ import type { LayerArgs, YConfig } from '../../common/expressions'; import type { LensMultiTable } from '../../common/types'; import { hasIcon } from './xy_config_panel/reference_line_panel'; -const REFERENCE_LINE_MARKER_SIZE = 20; +export const REFERENCE_LINE_MARKER_SIZE = 20; export const computeChartMargins = ( referenceLinePaddings: Partial>, @@ -180,6 +180,17 @@ function getMarkerToShow( } } +export interface ReferenceLineAnnotationsProps { + layers: LayerArgs[]; + data: LensMultiTable; + formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; + paletteService: PaletteRegistry; + syncColors: boolean; + axesMap: Record<'left' | 'right', boolean>; + isHorizontal: boolean; + paddingMap: Partial>; +} + export const ReferenceLineAnnotations = ({ layers, data, @@ -189,16 +200,7 @@ export const ReferenceLineAnnotations = ({ axesMap, isHorizontal, paddingMap, -}: { - layers: LayerArgs[]; - data: LensMultiTable; - formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - paletteService: PaletteRegistry; - syncColors: boolean; - axesMap: Record<'left' | 'right', boolean>; - isHorizontal: boolean; - paddingMap: Partial>; -}) => { +}: ReferenceLineAnnotationsProps) => { return ( <> {layers.flatMap((layer) => { @@ -317,10 +319,9 @@ export const ReferenceLineAnnotations = ({ id={`${layerId}-${yConfig.forAccessor}-rect`} key={`${layerId}-${yConfig.forAccessor}-rect`} dataValues={table.rows.map(() => { - const nextValue = - !isFillAbove && shouldCheckNextReferenceLine - ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] - : undefined; + const nextValue = shouldCheckNextReferenceLine + ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] + : undefined; if (yConfig.axisMode === 'bottom') { return { coordinates: { From 54fe3c580e4e51483a6c35f7c204ec7300903541 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 9 Nov 2021 13:09:00 -0500 Subject: [PATCH 53/98] [Fleet] Move settings to their own tab (#117919) --- .../fleet/public/applications/fleet/app.tsx | 32 +++------ .../fleet/hooks/use_breadcrumbs.tsx | 8 +++ .../fleet/layouts/default/default.tsx | 11 +++ .../fleet_server_on_prem_instructions.tsx | 16 ++--- .../applications/fleet/sections/index.tsx | 7 +- .../legacy_settings_form}/confirm_modal.tsx | 0 .../hosts_input.test.tsx | 2 +- .../legacy_settings_form}/hosts_input.tsx | 2 +- .../legacy_settings_form}/index.tsx | 58 ++++++---------- .../fleet/sections/settings/index.tsx | 23 +++++++ .../public/applications/integrations/app.tsx | 16 +---- .../agent_enrollment_flyout/index.tsx | 12 +--- .../missing_fleet_server_host_callout.tsx | 12 +--- .../plugins/fleet/public/components/index.ts | 1 - .../fleet/public/constants/page_paths.ts | 5 +- x-pack/plugins/fleet/public/hooks/index.ts | 1 - .../fleet/public/hooks/use_url_modal.ts | 67 ------------------- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 19 files changed, 94 insertions(+), 183 deletions(-) rename x-pack/plugins/fleet/public/{components/settings_flyout => applications/fleet/sections/settings/components/legacy_settings_form}/confirm_modal.tsx (100%) rename x-pack/plugins/fleet/public/{components/settings_flyout => applications/fleet/sections/settings/components/legacy_settings_form}/hosts_input.test.tsx (98%) rename x-pack/plugins/fleet/public/{components/settings_flyout => applications/fleet/sections/settings/components/legacy_settings_form}/hosts_input.tsx (98%) rename x-pack/plugins/fleet/public/{components/settings_flyout => applications/fleet/sections/settings/components/legacy_settings_form}/index.tsx (92%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx delete mode 100644 x-pack/plugins/fleet/public/hooks/use_url_modal.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 682c889d80b97..73be40c3c32d9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -8,7 +8,7 @@ import type { FunctionComponent } from 'react'; import React, { memo, useEffect, useState } from 'react'; import type { AppMountParameters } from 'kibana/public'; -import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel, EuiPortal } from '@elastic/eui'; +import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel } from '@elastic/eui'; import type { History } from 'history'; import { Router, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -25,7 +25,7 @@ import { } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; -import { PackageInstallProvider, useUrlModal } from '../integrations/hooks'; +import { PackageInstallProvider } from '../integrations/hooks'; import { ConfigContext, @@ -37,7 +37,7 @@ import { useStartServices, UIExtensionsContext, } from './hooks'; -import { Error, Loading, SettingFlyout, FleetSetupLoading } from './components'; +import { Error, Loading, FleetSetupLoading } from './components'; import type { UIExtensionsStorage } from './types'; import { FLEET_ROUTING_PATHS } from './constants'; @@ -48,6 +48,7 @@ import { AgentsApp } from './sections/agents'; import { MissingESRequirementsPage } from './sections/agents/agent_requirements_page'; import { CreatePackagePolicyPage } from './sections/agent_policy/create_package_policy_page'; import { EnrollmentTokenListPage } from './sections/agents/enrollment_token_list_page'; +import { SettingsApp } from './sections/settings'; const FEEDBACK_URL = 'https://ela.st/fleet-feedback'; @@ -244,7 +245,6 @@ export const FleetAppContext: React.FC<{ const FleetTopNav = memo( ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => { - const { getModalHref } = useUrlModal(); const services = useStartServices(); const { TopNavMenu } = services.navigation.ui; @@ -257,14 +257,6 @@ const FleetTopNav = memo( iconType: 'popout', run: () => window.open(FEEDBACK_URL), }, - - { - label: i18n.translate('xpack.fleet.appNavigation.settingsButton', { - defaultMessage: 'Fleet settings', - }), - iconType: 'gear', - run: () => services.application.navigateToUrl(getModalHref('settings')), - }, ]; return ( { - const { modal, setModal } = useUrlModal(); - return ( <> - {modal === 'settings' && ( - - { - setModal(null); - }} - /> - - )} - @@ -308,6 +288,10 @@ export const AppRoutes = memo( + + + + {/* TODO: Move this route to the Integrations app */} diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index 0202fc3351fc0..d77b243500100 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -147,6 +147,14 @@ const breadcrumbGetters: { }), }, ], + settings: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.fleet.breadcrumbs.settingsPageTitle', { + defaultMessage: 'Settings', + }), + }, + ], }; export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx index dd15020adcc75..c8dd428f0df5e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx @@ -78,6 +78,17 @@ export const DefaultLayout: React.FunctionComponent = ({ href: getHref('data_streams'), 'data-test-subj': 'fleet-datastreams-tab', }, + { + name: ( + + ), + isSelected: section === 'settings', + href: getHref('settings'), + 'data-test-subj': 'fleet-settings-tab', + }, ]} > {children} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx index bf4b1eb00abe0..e3bb252beb96f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -41,7 +41,7 @@ import { sendPutSettings, sendGetFleetStatus, useFleetStatus, - useUrlModal, + useLink, } from '../../../../hooks'; import type { PLATFORM_TYPE } from '../../../../hooks'; import type { PackagePolicy } from '../../../../types'; @@ -416,7 +416,8 @@ export const AddFleetServerHostStepContent = ({ const [isLoading, setIsLoading] = useState(false); const [fleetServerHost, setFleetServerHost] = useState(''); const [error, setError] = useState(); - const { getModalHref } = useUrlModal(); + + const { getHref } = useLink(); const validate = useCallback( (host: string) => { @@ -519,7 +520,7 @@ export const AddFleetServerHostStepContent = ({ values={{ host: calloutHost, fleetSettingsLink: ( - + { installCommand, platform, setPlatform, - refresh, deploymentMode, setDeploymentMode, fleetServerHost, addFleetServerHost, } = useFleetServerInstructions(policyId); - const { modal } = useUrlModal(); - useEffect(() => { - // Refresh settings when the settings modal is closed - if (!modal) { - refresh(); - } - }, [modal, refresh]); - const { docLinks } = useStartServices(); const [isWaitingForFleetServer, setIsWaitingForFleetServer] = useState(true); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/index.tsx index b36fbf4bb815e..848ceac11c001 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/index.tsx @@ -9,4 +9,9 @@ export { AgentPolicyApp } from './agent_policy'; export { DataStreamApp } from './data_stream'; export { AgentsApp } from './agents'; -export type Section = 'agents' | 'agent_policies' | 'enrollment_tokens' | 'data_streams'; +export type Section = + | 'agents' + | 'agent_policies' + | 'enrollment_tokens' + | 'data_streams' + | 'settings'; diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/confirm_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx similarity index 100% rename from x-pack/plugins/fleet/public/components/settings_flyout/confirm_modal.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx similarity index 98% rename from x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.test.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx index 01ec166b74afc..aca3399c4af46 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, act } from '@testing-library/react'; -import { createFleetTestRendererMock } from '../../mock'; +import { createFleetTestRendererMock } from '../../../../../../mock'; import { HostsInput } from './hosts_input'; diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx similarity index 98% rename from x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx index 49cff905d167f..30ef969aceec7 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/hosts_input.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/hosts_input.tsx @@ -27,7 +27,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { EuiTheme } from '../../../../../../src/plugins/kibana_react/common'; +import type { EuiTheme } from '../../../../../../../../../../src/plugins/kibana_react/common'; interface Props { id: string; diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx similarity index 92% rename from x-pack/plugins/fleet/public/components/settings_flyout/index.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx index d10fd8336a37f..6caca7209e0d2 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx @@ -9,16 +9,12 @@ import React, { useEffect, useCallback } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, + EuiPortal, EuiTitle, EuiFlexGroup, EuiFlexItem, - EuiButtonEmpty, EuiSpacer, EuiButton, - EuiFlyoutFooter, EuiForm, EuiFormRow, EuiCode, @@ -38,9 +34,9 @@ import { sendPutSettings, useDefaultOutput, sendPutOutput, -} from '../../hooks'; -import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common'; -import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public'; +} from '../../../../../../hooks'; +import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../../../../../common'; +import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { SettingsConfirmModal } from './confirm_modal'; import type { SettingsConfirmModalProps } from './confirm_modal'; @@ -68,10 +64,6 @@ const CodeEditorPlaceholder = styled(EuiTextColor).attrs((props) => ({ const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; -interface Props { - onClose: () => void; -} - function normalizeHosts(hostsInput: string[]) { return hostsInput.map((host) => { try { @@ -88,7 +80,7 @@ function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: stri return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]); } -function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { +function useSettingsForm(outputId: string | undefined) { const [isLoading, setIsloading] = React.useState(false); const { notifications } = useStartServices(); @@ -237,7 +229,6 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { }) ); setIsloading(false); - onSuccess(); } catch (error) { setIsloading(false); notifications.toasts.addError(error, { @@ -253,13 +244,13 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { }; } -export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { +export const LegacySettingsForm: React.FunctionComponent = () => { const { docLinks } = useStartServices(); const settingsRequest = useGetSettings(); const settings = settingsRequest?.data?.item; const { output } = useDefaultOutput(); - const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id, onClose); + const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id); const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false); @@ -455,14 +446,16 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { return ( <> {isConfirmModalVisible && ( - + + + )} - - + <> + <>

= ({ onClose }) => { />

-
- {body} - + + <>{body} + <> + - - - - - = ({ onClose }) => { - -
+ + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx new file mode 100644 index 0000000000000..6117d3249b189 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useBreadcrumbs } from '../../hooks'; +import { DefaultLayout } from '../../layouts'; + +import { LegacySettingsForm } from './components/legacy_settings_form'; + +export const SettingsApp = () => { + useBreadcrumbs('settings'); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/app.tsx b/x-pack/plugins/fleet/public/applications/integrations/app.tsx index 3a091c30bb792..eca2c0c0612c7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/app.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/app.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import type { AppMountParameters } from 'kibana/public'; -import { EuiErrorBoundary, EuiPortal } from '@elastic/eui'; +import { EuiErrorBoundary } from '@elastic/eui'; import type { History } from 'history'; import { Router, Redirect, Route, Switch } from 'react-router-dom'; import useObservable from 'react-use/lib/useObservable'; @@ -22,11 +22,9 @@ import { } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; -import { AgentPolicyContextProvider, useUrlModal } from './hooks'; +import { AgentPolicyContextProvider } from './hooks'; import { INTEGRATIONS_ROUTING_PATHS, pagePathGetters } from './constants'; -import { SettingFlyout } from './components'; - import type { UIExtensionsStorage } from './types'; import { EPMApp } from './sections/epm'; @@ -93,18 +91,8 @@ export const IntegrationsAppContext: React.FC<{ ); export const AppRoutes = memo(() => { - const { modal, setModal } = useUrlModal(); return ( <> - {modal === 'settings' && ( - - { - setModal(null); - }} - /> - - )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 87911e5d6c2c7..62bf3e8d6564a 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -22,7 +22,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useGetSettings, useUrlModal, sendGetOneAgentPolicy, useFleetStatus } from '../../hooks'; +import { useGetSettings, sendGetOneAgentPolicy, useFleetStatus } from '../../hooks'; import { FLEET_SERVER_PACKAGE } from '../../constants'; import type { PackagePolicy } from '../../types'; @@ -52,19 +52,9 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ }) => { const [mode, setMode] = useState(defaultMode); - const { modal } = useUrlModal(); - const [lastModal, setLastModal] = useState(modal); const settings = useGetSettings(); const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; - // Refresh settings when there is a modal/flyout change - useEffect(() => { - if (modal !== lastModal) { - settings.resendRequest(); - setLastModal(modal); - } - }, [modal, lastModal, settings]); - const fleetStatus = useFleetStatus(); const [policyId, setSelectedPolicyId] = useState(agentPolicy?.id); const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx index c390b50c498fb..220b98f07cd35 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx @@ -10,11 +10,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiButton, EuiSpacer } from '@elastic/eui'; -import { useUrlModal, useStartServices } from '../../hooks'; +import { useLink, useStartServices } from '../../hooks'; export const MissingFleetServerHostCallout: React.FunctionComponent = () => { - const { setModal } = useUrlModal(); const { docLinks } = useStartServices(); + const { getHref } = useLink(); return ( { }} /> - { - setModal('settings'); - }} - > + [FLEET_BASE_PATH, `/agents/${agentId}/logs`], enrollment_tokens: () => [FLEET_BASE_PATH, '/enrollment-tokens'], data_streams: () => [FLEET_BASE_PATH, '/data-streams'], + settings: () => [FLEET_BASE_PATH, '/settings'], }; diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index 16454a266c3c4..fa1f09fbf0b79 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -19,7 +19,6 @@ export { usePagination, PAGE_SIZE_OPTIONS } from './use_pagination'; export { useUrlPagination } from './use_url_pagination'; export { useSorting } from './use_sorting'; export { useDebounce } from './use_debounce'; -export { useUrlModal } from './use_url_modal'; export * from './use_request'; export * from './use_input'; export * from './use_url_params'; diff --git a/x-pack/plugins/fleet/public/hooks/use_url_modal.ts b/x-pack/plugins/fleet/public/hooks/use_url_modal.ts deleted file mode 100644 index b6bdba5eba844..0000000000000 --- a/x-pack/plugins/fleet/public/hooks/use_url_modal.ts +++ /dev/null @@ -1,67 +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 { useCallback, useMemo } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; - -import { useUrlParams } from './use_url_params'; - -type Modal = 'settings'; - -/** - * Uses URL params for pagination and also persists those to the URL as they are updated - */ -export const useUrlModal = () => { - const location = useLocation(); - const history = useHistory(); - const { urlParams, toUrlParams } = useUrlParams(); - - const setModal = useCallback( - (modal: Modal | null) => { - const newUrlParams: any = { - ...urlParams, - modal, - }; - - if (modal === null) { - delete newUrlParams.modal; - } - history.push({ - ...location, - search: toUrlParams(newUrlParams), - }); - }, - [history, location, toUrlParams, urlParams] - ); - - const getModalHref = useCallback( - (modal: Modal | null) => { - return history.createHref({ - ...location, - search: toUrlParams({ - ...urlParams, - modal, - }), - }); - }, - [history, location, toUrlParams, urlParams] - ); - - const modal: Modal | null = useMemo(() => { - if (urlParams.modal === 'settings') { - return urlParams.modal; - } - - return null; - }, [urlParams.modal]); - - return { - modal, - setModal, - getModalHref, - }; -}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8bc0cef3a44e5..7281a2a70fc6a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10679,7 +10679,6 @@ "xpack.fleet.appNavigation.integrationsInstalledLinkText": "管理", "xpack.fleet.appNavigation.policiesLinkText": "エージェントポリシー", "xpack.fleet.appNavigation.sendFeedbackButton": "フィードバックを送信", - "xpack.fleet.appNavigation.settingsButton": "Fleet 設定", "xpack.fleet.appTitle": "Fleet", "xpack.fleet.assets.customLogs.description": "ログアプリでカスタムログデータを表示", "xpack.fleet.assets.customLogs.name": "ログ", @@ -11058,7 +11057,6 @@ "xpack.fleet.serverError.returnedIncorrectKey": "find enrollmentKeyByIdで正しくないキーが返されました", "xpack.fleet.serverError.unableToCreateEnrollmentKey": "登録APIキーを作成できません", "xpack.fleet.settings.additionalYamlConfig": "Elasticsearch出力構成(YAML)", - "xpack.fleet.settings.cancelButtonLabel": "キャンセル", "xpack.fleet.settings.deleteHostButton": "ホストの削除", "xpack.fleet.settings.elasticHostError": "無効なURL", "xpack.fleet.settings.elasticsearchUrlLabel": "Elasticsearchホスト", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d3de9dab0df52..b6b2744160423 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10786,7 +10786,6 @@ "xpack.fleet.appNavigation.integrationsInstalledLinkText": "管理", "xpack.fleet.appNavigation.policiesLinkText": "代理策略", "xpack.fleet.appNavigation.sendFeedbackButton": "发送反馈", - "xpack.fleet.appNavigation.settingsButton": "Fleet 设置", "xpack.fleet.appTitle": "Fleet", "xpack.fleet.assets.customLogs.description": "在 Logs 应用中查看定制日志", "xpack.fleet.assets.customLogs.name": "日志", @@ -11171,7 +11170,6 @@ "xpack.fleet.serverError.returnedIncorrectKey": "find enrollmentKeyById 返回错误的密钥", "xpack.fleet.serverError.unableToCreateEnrollmentKey": "无法创建注册 api 密钥", "xpack.fleet.settings.additionalYamlConfig": "Elasticsearch 输出配置 (YAML)", - "xpack.fleet.settings.cancelButtonLabel": "取消", "xpack.fleet.settings.deleteHostButton": "删除主机", "xpack.fleet.settings.elasticHostError": "URL 无效", "xpack.fleet.settings.elasticsearchUrlLabel": "Elasticsearch 主机", From 13d651bf625a25e4b1d629c9b6c5907c8cced35f Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 9 Nov 2021 19:13:53 +0100 Subject: [PATCH 54/98] [Security Solution] Copy changes for host isolation exceptions form (#118021) --- .../host_isolation_exceptions/view/components/form.tsx | 4 ++-- .../view/components/form_flyout.tsx | 8 ++++---- .../view/components/translations.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx index 01fe8583bae60..4e853f0e6fa6f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -179,7 +179,7 @@ export const HostIsolationExceptionsForm: React.FC<{ @@ -198,7 +198,7 @@ export const HostIsolationExceptionsForm: React.FC<{ diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx index 2eeddbaeeb0f3..0ee77f1b408a3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -181,12 +181,12 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => { {exception?.item_id ? ( ) : ( )} @@ -206,14 +206,14 @@ export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => {

) : (

)} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts index 69f2c7809a52a..9504aa0673e54 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts @@ -11,7 +11,7 @@ import { ServerApiError } from '../../../../../common/types'; export const NAME_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.form.name.placeholder', { - defaultMessage: 'New IP', + defaultMessage: 'Host isolation exception name', } ); @@ -32,7 +32,7 @@ export const NAME_ERROR = i18n.translate( export const DESCRIPTION_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.hostIsolationExceptions.form.description.placeholder', { - defaultMessage: 'Describe your Host isolation exception', + defaultMessage: 'Describe your host isolation exception', } ); From 27f5ff326d1b10cbc09328115ddcf6063338a64e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 9 Nov 2021 18:24:43 +0000 Subject: [PATCH 55/98] chore(NA): moves disk_cache and repository_cache bazel flags from common to specific commands (#118037) --- .bazelrc.common | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.bazelrc.common b/.bazelrc.common index 0ad0c95fdcbbd..e210b06ed2706 100644 --- a/.bazelrc.common +++ b/.bazelrc.common @@ -14,9 +14,18 @@ query --experimental_guard_against_concurrent_changes ## Cache action outputs on disk so they persist across output_base and bazel shutdown (eg. changing branches) build --disk_cache=~/.bazel-cache/disk-cache +fetch --disk_cache=~/.bazel-cache/disk-cache +query --disk_cache=~/.bazel-cache/disk-cache +sync --disk_cache=~/.bazel-cache/disk-cache +test --disk_cache=~/.bazel-cache/disk-cache ## Bazel repo cache settings build --repository_cache=~/.bazel-cache/repository-cache +fetch --repository_cache=~/.bazel-cache/repository-cache +query --repository_cache=~/.bazel-cache/repository-cache +run --repository_cache=~/.bazel-cache/repository-cache +sync --repository_cache=~/.bazel-cache/repository-cache +test --repository_cache=~/.bazel-cache/repository-cache # Bazel will create symlinks from the workspace directory to output artifacts. # Build results will be placed in a directory called "bazel-bin" From 92e462c5e19d0ec25e8dda22e30adc9f347e3e8f Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Tue, 9 Nov 2021 19:28:15 +0100 Subject: [PATCH 56/98] adds 'sec-eng-prod' as owners from some of the security solution cypress tests (#118055) Co-authored-by: Gloria Hornero Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 990cfef20eae0..d532d4b681c77 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -405,6 +405,12 @@ /x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/ @elastic/security-onboarding-and-lifecycle-mgt /x-pack/test/security_solution_endpoint/apps/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt +## Security Solution sub teams - security-engineering-productivity +x-pack/plugins/security_solution/cypress/ccs_integration +x-pack/plugins/security_solution/cypress/upgrade_integration +x-pack/plugins/security_solution/cypress/README.md +x-pack/test/security_solution_cypress + # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics From 29148d3ed7f2169e2aff702432fe64fef1d9b04f Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 9 Nov 2021 19:35:37 +0100 Subject: [PATCH 57/98] [Security Solution] Fix/host isolation exceptions header (#118041) * Copy changes for host isolation exceptions form * Fix hasDataToShow condition to show search bar and header * Remove copy changes from other branch --- .../view/host_isolation_exceptions_list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index 815d790b5b4af..f7429c213d2d5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -60,7 +60,7 @@ export const HostIsolationExceptionsList = () => { const history = useHistory(); const privileges = useEndpointPrivileges(); const showFlyout = privileges.canIsolateHost && !!location.show; - const hasDataToShow = !isLoading && (!!location.filter || listItems.length > 0); + const hasDataToShow = !!location.filter || listItems.length > 0; useEffect(() => { if (!isLoading && listItems.length === 0 && !privileges.canIsolateHost) { From 4e443cca4fb933d40ecd3b4d7583ace6f6d397a1 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 9 Nov 2021 11:42:39 -0700 Subject: [PATCH 58/98] [kbn/io-ts] export and require importing individual functions (#117958) --- .eslintrc.js | 4 ++++ packages/kbn-io-ts-utils/BUILD.bazel | 11 +++++++++++ packages/kbn-io-ts-utils/deep_exact_rt/package.json | 4 ++++ packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json | 4 ++++ packages/kbn-io-ts-utils/json_rt/package.json | 4 ++++ packages/kbn-io-ts-utils/merge_rt/package.json | 4 ++++ .../kbn-io-ts-utils/non_empty_string_rt/package.json | 4 ++++ packages/kbn-io-ts-utils/parseable_types/package.json | 4 ++++ packages/kbn-io-ts-utils/props_to_schema/package.json | 4 ++++ packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts | 2 +- packages/kbn-io-ts-utils/strict_keys_rt/package.json | 4 ++++ packages/kbn-io-ts-utils/to_boolean_rt/package.json | 4 ++++ packages/kbn-io-ts-utils/to_json_schema/package.json | 4 ++++ packages/kbn-io-ts-utils/to_number_rt/package.json | 4 ++++ .../src/decode_request_params.test.ts | 4 ++-- .../src/decode_request_params.ts | 2 +- .../src/create_router.test.tsx | 2 +- .../src/create_router.ts | 10 ++-------- x-pack/plugins/apm/common/environment_rt.ts | 2 +- .../apm/public/components/routing/home/index.tsx | 2 +- .../components/routing/service_detail/index.tsx | 2 +- x-pack/plugins/apm/server/routes/backends.ts | 2 +- x-pack/plugins/apm/server/routes/default_api_types.ts | 2 +- .../plugins/apm/server/routes/latency_distribution.ts | 2 +- .../apm/server/routes/observability_overview.ts | 2 +- .../apm/server/routes/register_routes/index.test.ts | 2 +- .../apm/server/routes/register_routes/index.ts | 3 ++- x-pack/plugins/apm/server/routes/rum_client.ts | 2 +- x-pack/plugins/apm/server/routes/services.ts | 4 +++- .../apm/server/routes/settings/agent_configuration.ts | 2 +- x-pack/plugins/apm/server/routes/source_maps.ts | 2 +- x-pack/plugins/apm/server/routes/transactions.ts | 3 ++- 32 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 packages/kbn-io-ts-utils/deep_exact_rt/package.json create mode 100644 packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json create mode 100644 packages/kbn-io-ts-utils/json_rt/package.json create mode 100644 packages/kbn-io-ts-utils/merge_rt/package.json create mode 100644 packages/kbn-io-ts-utils/non_empty_string_rt/package.json create mode 100644 packages/kbn-io-ts-utils/parseable_types/package.json create mode 100644 packages/kbn-io-ts-utils/props_to_schema/package.json create mode 100644 packages/kbn-io-ts-utils/strict_keys_rt/package.json create mode 100644 packages/kbn-io-ts-utils/to_boolean_rt/package.json create mode 100644 packages/kbn-io-ts-utils/to_json_schema/package.json create mode 100644 packages/kbn-io-ts-utils/to_number_rt/package.json diff --git a/.eslintrc.js b/.eslintrc.js index 00c96e5cf0491..e338c28f7f848 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -226,6 +226,10 @@ const RESTRICTED_IMPORTS = [ name: 'react-use', message: 'Please use react-use/lib/{method} instead.', }, + { + name: '@kbn/io-ts-utils', + message: `Import directly from @kbn/io-ts-utils/{method} submodules`, + }, ]; module.exports = { diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index 474fa2c2bb121..e5f1de4d07f63 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -23,6 +23,17 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", + "deep_exact_rt/package.json", + "iso_to_epoch_rt/package.json", + "json_rt/package.json", + "merge_rt/package.json", + "non_empty_string_rt/package.json", + "parseable_types/package.json", + "props_to_schema/package.json", + "strict_keys_rt/package.json", + "to_boolean_rt/package.json", + "to_json_schema/package.json", + "to_number_rt/package.json", ] RUNTIME_DEPS = [ diff --git a/packages/kbn-io-ts-utils/deep_exact_rt/package.json b/packages/kbn-io-ts-utils/deep_exact_rt/package.json new file mode 100644 index 0000000000000..b42591a2e82d0 --- /dev/null +++ b/packages/kbn-io-ts-utils/deep_exact_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/deep_exact_rt", + "types": "../target_types/deep_exact_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json b/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json new file mode 100644 index 0000000000000..e96c50b9fbf4e --- /dev/null +++ b/packages/kbn-io-ts-utils/iso_to_epoch_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/iso_to_epoch_rt", + "types": "../target_types/iso_to_epoch_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/json_rt/package.json b/packages/kbn-io-ts-utils/json_rt/package.json new file mode 100644 index 0000000000000..f896827cf99a4 --- /dev/null +++ b/packages/kbn-io-ts-utils/json_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/json_rt", + "types": "../target_types/json_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/merge_rt/package.json b/packages/kbn-io-ts-utils/merge_rt/package.json new file mode 100644 index 0000000000000..f7773688068e0 --- /dev/null +++ b/packages/kbn-io-ts-utils/merge_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/merge_rt", + "types": "../target_types/merge_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/non_empty_string_rt/package.json b/packages/kbn-io-ts-utils/non_empty_string_rt/package.json new file mode 100644 index 0000000000000..6348f6d728059 --- /dev/null +++ b/packages/kbn-io-ts-utils/non_empty_string_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/non_empty_string_rt", + "types": "../target_types/non_empty_string_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/parseable_types/package.json b/packages/kbn-io-ts-utils/parseable_types/package.json new file mode 100644 index 0000000000000..6dab2a5ee156e --- /dev/null +++ b/packages/kbn-io-ts-utils/parseable_types/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/parseable_types", + "types": "../target_types/parseable_types" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/props_to_schema/package.json b/packages/kbn-io-ts-utils/props_to_schema/package.json new file mode 100644 index 0000000000000..478de84d17f81 --- /dev/null +++ b/packages/kbn-io-ts-utils/props_to_schema/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/props_to_schema", + "types": "../target_types/props_to_schema" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts index cb3d9bb2100d0..28fdc89751fd4 100644 --- a/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts +++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts @@ -113,7 +113,7 @@ export function strictKeysRt(type: T) { const excessKeys = difference([...keys.all], [...keys.handled]); if (excessKeys.length) { - return t.failure(i, context, `Excess keys are not allowed: \n${excessKeys.join('\n')}`); + return t.failure(i, context, `Excess keys are not allowed:\n${excessKeys.join('\n')}`); } return t.success(i); diff --git a/packages/kbn-io-ts-utils/strict_keys_rt/package.json b/packages/kbn-io-ts-utils/strict_keys_rt/package.json new file mode 100644 index 0000000000000..68823d97a5d00 --- /dev/null +++ b/packages/kbn-io-ts-utils/strict_keys_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/strict_keys_rt", + "types": "../target_types/strict_keys_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_boolean_rt/package.json b/packages/kbn-io-ts-utils/to_boolean_rt/package.json new file mode 100644 index 0000000000000..5e801a6529153 --- /dev/null +++ b/packages/kbn-io-ts-utils/to_boolean_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/to_boolean_rt", + "types": "../target_types/to_boolean_rt" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_json_schema/package.json b/packages/kbn-io-ts-utils/to_json_schema/package.json new file mode 100644 index 0000000000000..366f3243b1156 --- /dev/null +++ b/packages/kbn-io-ts-utils/to_json_schema/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/to_json_schema", + "types": "../target_types/to_json_schema" +} \ No newline at end of file diff --git a/packages/kbn-io-ts-utils/to_number_rt/package.json b/packages/kbn-io-ts-utils/to_number_rt/package.json new file mode 100644 index 0000000000000..f5da955cb9775 --- /dev/null +++ b/packages/kbn-io-ts-utils/to_number_rt/package.json @@ -0,0 +1,4 @@ +{ + "main": "../target_node/to_number_rt", + "types": "../target_types/to_number_rt" +} \ No newline at end of file diff --git a/packages/kbn-server-route-repository/src/decode_request_params.test.ts b/packages/kbn-server-route-repository/src/decode_request_params.test.ts index 08ef303ad0b3a..a5c1a2b49eb19 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.test.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.test.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { jsonRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; import * as t from 'io-ts'; import { decodeRequestParams } from './decode_request_params'; @@ -69,7 +69,7 @@ describe('decodeRequestParams', () => { }; expect(decode).toThrowErrorMatchingInlineSnapshot(` - "Excess keys are not allowed: + "Excess keys are not allowed: path.extraKey" `); }); diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts index 00492d69b8ac5..4df6fa3333c50 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.ts @@ -10,7 +10,7 @@ import { omitBy, isPlainObject, isEmpty } from 'lodash'; import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import Boom from '@hapi/boom'; -import { strictKeysRt } from '@kbn/io-ts-utils'; +import { strictKeysRt } from '@kbn/io-ts-utils/strict_keys_rt'; import { RouteParamsRT } from './typings'; interface KibanaRequestParams { 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 9837d45ddd869..ac337f8bb5b87 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'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; 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 77c2bba14e85a..89ff4fc6b0c6c 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -15,16 +15,10 @@ import { } from 'react-router-config'; import qs from 'query-string'; import { findLastIndex, merge, compact } from 'lodash'; -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 { mergeRt } from '@kbn/io-ts-utils/merge_rt'; +import { deepExactRt } from '@kbn/io-ts-utils/deep_exact_rt'; import { FlattenRoutesOf, Route, Router } from './types'; -const deepExactRt: typeof deepExactRtTyped = deepExactRtNonTyped; -const mergeRt: typeof mergeRtTyped = mergeRtNonTyped; - function toReactRouterPath(path: string) { return path.replace(/(?:{([^\/]+)})/g, ':$1'); } diff --git a/x-pack/plugins/apm/common/environment_rt.ts b/x-pack/plugins/apm/common/environment_rt.ts index e9337da9bdcf5..4598ffa6f6681 100644 --- a/x-pack/plugins/apm/common/environment_rt.ts +++ b/x-pack/plugins/apm/common/environment_rt.ts @@ -5,7 +5,7 @@ * 2.0. */ import * as t from 'io-ts'; -import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import { nonEmptyStringRt } from '@kbn/io-ts-utils/non_empty_string_rt'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED, 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 25a68592d2b11..e70cb31eef88f 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Outlet } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; -import { toBooleanRt } from '@kbn/io-ts-utils'; +import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; import { RedirectTo } from '../redirect_to'; import { comparisonTypeRt } from '../../../../common/runtime_types/comparison_type_rt'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; 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 37259f7c91e22..d8a996d2163bc 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,7 +8,7 @@ import * as t from 'io-ts'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Outlet } from '@kbn/typed-react-router-config'; -import { toBooleanRt } from '@kbn/io-ts-utils'; +import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; import { comparisonTypeRt } from '../../../../common/runtime_types/comparison_type_rt'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { environmentRt } from '../../../../common/environment_rt'; diff --git a/x-pack/plugins/apm/server/routes/backends.ts b/x-pack/plugins/apm/server/routes/backends.ts index 03466c7443665..4dcd8a1db9575 100644 --- a/x-pack/plugins/apm/server/routes/backends.ts +++ b/x-pack/plugins/apm/server/routes/backends.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import { setupRequest } from '../lib/helpers/setup_request'; import { environmentRt, kueryRt, offsetRt, rangeRt } from './default_api_types'; import { createApmServerRoute } from './create_apm_server_route'; 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 5622b12e1b099..b31de8e53dad2 100644 --- a/x-pack/plugins/apm/server/routes/default_api_types.ts +++ b/x-pack/plugins/apm/server/routes/default_api_types.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { isoToEpochRt } from '@kbn/io-ts-utils'; +import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; export { environmentRt } from '../../common/environment_rt'; diff --git a/x-pack/plugins/apm/server/routes/latency_distribution.ts b/x-pack/plugins/apm/server/routes/latency_distribution.ts index 128192d0464c7..ba646c0fc92bb 100644 --- a/x-pack/plugins/apm/server/routes/latency_distribution.ts +++ b/x-pack/plugins/apm/server/routes/latency_distribution.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import { getOverallLatencyDistribution } from '../lib/latency/get_overall_latency_distribution'; import { setupRequest } from '../lib/helpers/setup_request'; import { createApmServerRoute } from './create_apm_server_route'; diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index 2aff798f9ad0b..b4fb4804a9bda 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { toNumberRt } from '@kbn/io-ts-utils'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute'; diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts index 6cee6d8cad920..fd554ce1f335c 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { jsonRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; import { createServerRouteRepository } from '@kbn/server-route-repository'; import { ServerRoute } from '@kbn/server-route-repository'; import * as t from 'io-ts'; diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts index 576c23dc0882f..6ac4b3ac18e24 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -17,7 +17,8 @@ import { parseEndpoint, routeValidationObject, } from '@kbn/server-route-repository'; -import { mergeRt, jsonRt } from '@kbn/io-ts-utils'; +import { mergeRt } from '@kbn/io-ts-utils/merge_rt'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; import { pickKeys } from '../../../common/utils/pick_keys'; import { APMRouteHandlerResources, TelemetryUsageCounter } from '../typings'; import type { ApmPluginRequestHandlerContext } from '../typings'; diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index d1b7e9233e9c8..c465e0e02da1c 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; import { Logger } from 'kibana/server'; -import { isoToEpochRt } from '@kbn/io-ts-utils'; +import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; import { setupRequest, Setup } from '../lib/helpers/setup_request'; import { getClientMetrics } from '../lib/rum_client/get_client_metrics'; import { getJSErrors } from '../lib/rum_client/get_js_errors'; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 3af829d59d3fd..60036a9a3da76 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -6,7 +6,9 @@ */ import Boom from '@hapi/boom'; -import { jsonRt, isoToEpochRt, toNumberRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { isoToEpochRt } from '@kbn/io-ts-utils/iso_to_epoch_rt'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import * as t from 'io-ts'; import { uniq } from 'lodash'; import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types'; 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 0488d0ebd01bd..5385cd74cd779 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; -import { toBooleanRt } from '@kbn/io-ts-utils'; +import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; import { maxSuggestions } from '../../../../observability/common'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getServiceNames } from '../../lib/settings/agent_configuration/get_service_names'; diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts index e0f872239b623..d009b68c6919b 100644 --- a/x-pack/plugins/apm/server/routes/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/source_maps.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; import { SavedObjectsClientContract } from 'kibana/server'; -import { jsonRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; import { createApmArtifact, deleteApmArtifact, diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 56b7ead2254d3..4f4ca7eb1ba85 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { jsonRt, toNumberRt } from '@kbn/io-ts-utils'; +import { jsonRt } from '@kbn/io-ts-utils/json_rt'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import * as t from 'io-ts'; import { LatencyAggregationType, From b122a23d043b2a379731a43fdff335e52fbdcc7f Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 9 Nov 2021 14:00:27 -0500 Subject: [PATCH 59/98] [Fleet] Show callout when EPR unavailable (#117598) --- ...-plugin-core-public.doclinksstart.links.md | 2 + .../public/doc_links/doc_links_service.ts | 4 + src/core/public/public.api.md | 2 + .../epm/screens/home/available_packages.tsx | 109 ++++++++++++++++-- .../plugins/fleet/server/errors/handlers.ts | 9 +- 5 files changed, 115 insertions(+), 11 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 5882543c6496e..59d1f13ed89fb 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -243,6 +243,7 @@ readonly links: { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -252,6 +253,7 @@ readonly links: { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index c459f96d402f5..6345f310d25da 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -489,6 +489,7 @@ export class DocLinksService { fleetServerAddFleetServer: `${FLEET_DOCS}fleet-server.html#add-fleet-server`, settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + settingsFleetServerProxySettings: `${KIBANA_DOCS}fleet-settings-kb.html#fleet-data-visualizer-settings`, troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, elasticAgent: `${FLEET_DOCS}elastic-agent-installation.html`, beatsAgentComparison: `${FLEET_DOCS}beats-agent-comparison.html`, @@ -500,6 +501,7 @@ export class DocLinksService { upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`, learnMoreBlog: `${ELASTIC_WEBSITE_URL}blog/elastic-agent-and-fleet-make-it-easier-to-integrate-your-systems-with-elastic`, apiKeysLearnMore: `${KIBANA_DOCS}api-keys.html`, + onPremRegistry: `${ELASTIC_WEBSITE_URL}guide/en/integrations-developer/${DOC_LINK_VERSION}/air-gapped.html`, }, ecs: { guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`, @@ -765,6 +767,7 @@ export interface DocLinksStart { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -774,6 +777,7 @@ export interface DocLinksStart { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 4eea3e1e475cf..26df3ee28d5c5 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -711,6 +711,7 @@ export interface DocLinksStart { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -720,6 +721,7 @@ export interface DocLinksStart { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index ca932554290bb..e54ab0d9ecd46 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -5,10 +5,12 @@ * 2.0. */ +import type { FunctionComponent } from 'react'; import React, { memo, useMemo, useState } from 'react'; import { useLocation, useHistory, useParams } from 'react-router-dom'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiFlexItem, @@ -16,6 +18,8 @@ import { EuiSpacer, EuiCard, EuiIcon, + EuiCallOut, + EuiLink, } from '@elastic/eui'; import { useStartServices } from '../../../../hooks'; @@ -52,6 +56,76 @@ import type { CategoryFacet } from './category_facets'; import type { CategoryParams } from '.'; import { getParams, categoryExists, mapToCard } from '.'; +const NoEprCallout: FunctionComponent<{ statusCode?: number }> = ({ + statusCode, +}: { + statusCode?: number; +}) => { + let titleMessage; + let descriptionMessage; + if (statusCode === 502) { + titleMessage = i18n.translate('xpack.fleet.epmList.eprUnavailableBadGatewayCalloutTitle', { + defaultMessage: + 'Kibana cannot reach the Elastic Package Registry, which provides Elastic Agent integrations\n', + }); + descriptionMessage = ( + , + onpremregistry: , + }} + /> + ); + } else { + titleMessage = i18n.translate('xpack.fleet.epmList.eprUnavailable400500CalloutTitle', { + defaultMessage: + 'Kibana cannot connect to the Elastic Package Registry, which provides Elastic Agent integrations\n', + }); + descriptionMessage = ( + , + onpremregistry: , + }} + /> + ); + } + + return ( + +

{descriptionMessage}

+
+ ); +}; + +function ProxyLink() { + const { docLinks } = useStartServices(); + + return ( + + {i18n.translate('xpack.fleet.epmList.proxyLinkSnippedText', { + defaultMessage: 'proxy server', + })} + + ); +} + +function OnPremLink() { + const { docLinks } = useStartServices(); + + return ( + + {i18n.translate('xpack.fleet.epmList.onPremLinkSnippetText', { + defaultMessage: 'your own registry', + })} + + ); +} + function getAllCategoriesFromIntegrations(pkg: PackageListItem) { if (!doesPackageHaveIntegrations(pkg)) { return pkg.categories; @@ -133,10 +207,13 @@ export const AvailablePackages: React.FC = memo(() => { history.replace(pagePathGetters.integrations_all({ searchTerm: search })[1]); } - const { data: eprPackages, isLoading: isLoadingAllPackages } = useGetPackages({ + const { + data: eprPackages, + isLoading: isLoadingAllPackages, + error: eprPackageLoadingError, + } = useGetPackages({ category: '', }); - const eprIntegrationList = useMemo( () => packageListToIntegrationsList(eprPackages?.response || []), [eprPackages] @@ -166,18 +243,23 @@ export const AvailablePackages: React.FC = memo(() => { return a.title.localeCompare(b.title); }); - const { data: eprCategories, isLoading: isLoadingCategories } = useGetCategories({ + const { + data: eprCategories, + isLoading: isLoadingCategories, + error: eprCategoryLoadingError, + } = useGetCategories({ include_policy_templates: true, }); const categories = useMemo(() => { - const eprAndCustomCategories: CategoryFacet[] = - isLoadingCategories || !eprCategories - ? [] - : mergeCategoriesAndCount( - eprCategories.response as Array<{ id: string; title: string; count: number }>, - cards - ); + const eprAndCustomCategories: CategoryFacet[] = isLoadingCategories + ? [] + : mergeCategoriesAndCount( + eprCategories + ? (eprCategories.response as Array<{ id: string; title: string; count: number }>) + : [], + cards + ); return [ { ...ALL_CATEGORY, @@ -281,6 +363,12 @@ export const AvailablePackages: React.FC = memo(() => { ); + let noEprCallout; + if (eprPackageLoadingError || eprCategoryLoadingError) { + const error = eprPackageLoadingError || eprCategoryLoadingError; + noEprCallout = ; + } + return ( { setSelectedCategory={setSelectedCategory} onSearchChange={setSearchTerm} showMissingIntegrationMessage + callout={noEprCallout} /> ); }); diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index c47b1a07780ec..e171a5bafba90 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -24,7 +24,9 @@ import { IngestManagerError, PackageNotFoundError, PackageUnsupportedMediaTypeError, + RegistryConnectionError, RegistryError, + RegistryResponseError, } from './index'; type IngestErrorHandler = ( @@ -40,7 +42,12 @@ interface IngestErrorHandlerParams { // this type is based on BadRequest values observed while debugging https://github.com/elastic/kibana/issues/75862 const getHTTPResponseCode = (error: IngestManagerError): number => { - if (error instanceof RegistryError) { + if (error instanceof RegistryResponseError) { + // 4xx/5xx's from EPR + return 500; + } + if (error instanceof RegistryConnectionError || error instanceof RegistryError) { + // Connection errors (ie. RegistryConnectionError) / fallback (RegistryError) from EPR return 502; // Bad Gateway } if (error instanceof PackageNotFoundError) { From d1d5e79398aa7154a84455527c5018c52f78b442 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Tue, 9 Nov 2021 19:04:54 +0000 Subject: [PATCH 60/98] [Security Solution][Detections] Improves Table UI (consistent dates) (#117643) [Security Solution][Detections] Improves Table UI (consistent dates) (#117643) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_solution/common/constants.ts | 1 + .../components/formatted_date/index.test.tsx | 23 +++++++++++ .../components/formatted_date/index.tsx | 23 ++++++++--- .../detection_engine/rules/all/columns.tsx | 38 ++++++++++++------- .../rules/all/exceptions/columns.tsx | 19 ++++++++++ 5 files changed, 85 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5b846751d26df..071d01a1bd557 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -69,6 +69,7 @@ export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000 as const; // ms export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100 as const; export const SECURITY_FEATURE_ID = 'Security' as const; export const DEFAULT_SPACE_ID = 'default' as const; +export const DEFAULT_RELATIVE_DATE_THRESHOLD = 24 as const; // Document path where threat indicator fields are expected. Fields are used // to enrich signals, and are copied to threat.enrichments. diff --git a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.test.tsx index 2a7b43dc0aa49..d25294879a572 100644 --- a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.test.tsx @@ -166,5 +166,28 @@ describe('formatted_date', () => { expect(wrapper.text()).toBe(getEmptyValue()); }); + + test('renders time as relative under 24hrs, configured through relativeThresholdInHrs', () => { + const timeThwentyThreeHrsAgo = new Date( + new Date().getTime() - 23 * 60 * 60 * 1000 + ).toISOString(); + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="relative-time"]').exists()).toBe(true); + }); + + test('renders time as absolute over 24hrs, configured through relativeThresholdInHrs', () => { + const timeThirtyHrsAgo = new Date(new Date().getTime() - 30 * 60 * 60 * 1000).toISOString(); + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="preference-time"]').exists()).toBe(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx index e525003660a85..41615c3f092bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx @@ -124,11 +124,14 @@ export interface FormattedRelativePreferenceDateProps { * @see https://momentjs.com/docs/#/displaying/format/ */ dateFormat?: string; + relativeThresholdInHrs?: number; + tooltipFieldName?: string; + tooltipAnchorClassName?: string; } /** - * Renders the specified date value according to under/over one hour - * Under an hour = relative format - * Over an hour = in a format determined by the user's preferences (can be overridden via prop), + * Renders the specified date value according to under/over configured by relativeThresholdInHrs in hours (default 1 hr) + * Under the relativeThresholdInHrs = relative format + * Over the relativeThresholdInHrs = in a format determined by the user's preferences (can be overridden via prop), * with a tooltip that renders: * - the name of the field * - a humanized relative date (e.g. 16 minutes ago) @@ -136,7 +139,7 @@ export interface FormattedRelativePreferenceDateProps { * - the raw date value (e.g. 2019-03-22T00:47:46Z) */ export const FormattedRelativePreferenceDate = React.memo( - ({ value, dateFormat }) => { + ({ value, dateFormat, tooltipFieldName, tooltipAnchorClassName, relativeThresholdInHrs = 1 }) => { if (value == null) { return getOrEmptyTagFromValue(value); } @@ -145,9 +148,17 @@ export const FormattedRelativePreferenceDate = React.memo - {moment(date).add(1, 'hours').isBefore(new Date()) ? ( + + {shouldDisplayPreferenceTime ? ( - - + ); }, width: '14%', @@ -228,9 +234,12 @@ export const getColumns = ({ return value == null ? ( getEmptyTagValue() ) : ( - - - + ); }, sortable: true, @@ -410,9 +419,12 @@ export const getMonitoringColumns = ( return value == null ? ( getEmptyTagValue() ) : ( - - - + ); }, width: '20%', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index 6d6fd974b20f5..3ba5db820544f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -9,7 +9,10 @@ import React from 'react'; import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui'; import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; +import { DEFAULT_RELATIVE_DATE_THRESHOLD } from '../../../../../../../common/constants'; import { FormatUrl } from '../../../../../../common/components/link_to'; +import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date'; + import * as i18n from './translations'; import { ExceptionListInfo } from './use_all_exception_lists'; import { ExceptionOverflowDisplay } from './exceptions_overflow_display'; @@ -84,6 +87,14 @@ export const getAllExceptionListsColumns = ( truncateText: true, dataType: 'date', width: '14%', + render: (value: ExceptionListInfo['created_at']) => ( + + ), }, { align: 'left', @@ -91,6 +102,14 @@ export const getAllExceptionListsColumns = ( name: i18n.LIST_DATE_UPDATED_TITLE, truncateText: true, width: '14%', + render: (value: ExceptionListInfo['updated_at']) => ( + + ), }, { align: 'center', From 195e5bebcbc29a6ae12c8c2b0e8066061ccca60c Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 9 Nov 2021 14:24:36 -0500 Subject: [PATCH 61/98] [CI] Add firefox and accessibility tasks to the flaky test suite runner (#118036) --- .buildkite/pipelines/flaky_tests/pipeline.js | 5 ++ .buildkite/pipelines/flaky_tests/runner.js | 77 ++++++++++++++------ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/.buildkite/pipelines/flaky_tests/pipeline.js b/.buildkite/pipelines/flaky_tests/pipeline.js index 208924aefe80e..37e8a97406eb5 100644 --- a/.buildkite/pipelines/flaky_tests/pipeline.js +++ b/.buildkite/pipelines/flaky_tests/pipeline.js @@ -23,11 +23,16 @@ for (let i = 1; i <= OSS_CI_GROUPS; i++) { inputs.push(stepInput(`oss/cigroup/${i}`, `OSS CI Group ${i}`)); } +inputs.push(stepInput(`oss/firefox`, 'OSS Firefox')); +inputs.push(stepInput(`oss/accessibility`, 'OSS Accessibility')); + for (let i = 1; i <= XPACK_CI_GROUPS; i++) { inputs.push(stepInput(`xpack/cigroup/${i}`, `Default CI Group ${i}`)); } inputs.push(stepInput(`xpack/cigroup/Docker`, 'Default CI Group Docker')); +inputs.push(stepInput(`xpack/firefox`, 'Default Firefox')); +inputs.push(stepInput(`xpack/accessibility`, 'Default Accessibility')); const pipeline = { steps: [ diff --git a/.buildkite/pipelines/flaky_tests/runner.js b/.buildkite/pipelines/flaky_tests/runner.js index bdb163504f46c..b5ccab137fd01 100644 --- a/.buildkite/pipelines/flaky_tests/runner.js +++ b/.buildkite/pipelines/flaky_tests/runner.js @@ -65,34 +65,67 @@ for (const testSuite of testSuites) { const JOB_PARTS = TEST_SUITE.split('/'); const IS_XPACK = JOB_PARTS[0] === 'xpack'; + const TASK = JOB_PARTS[1]; const CI_GROUP = JOB_PARTS.length > 2 ? JOB_PARTS[2] : ''; if (RUN_COUNT < 1) { continue; } - if (IS_XPACK) { - steps.push({ - command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, - label: `Default CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-6' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - } else { - steps.push({ - command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/oss_cigroup.sh`, - label: `OSS CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-4d' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); + switch (TASK) { + case 'cigroup': + if (IS_XPACK) { + steps.push({ + command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, + label: `Default CI Group ${CI_GROUP}`, + agents: { queue: 'ci-group-6' }, + depends_on: 'build', + parallelism: RUN_COUNT, + concurrency: concurrency, + concurrency_group: UUID, + concurrency_method: 'eager', + }); + } else { + steps.push({ + command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/oss_cigroup.sh`, + label: `OSS CI Group ${CI_GROUP}`, + agents: { queue: 'ci-group-4d' }, + depends_on: 'build', + parallelism: RUN_COUNT, + concurrency: concurrency, + concurrency_group: UUID, + concurrency_method: 'eager', + }); + } + break; + + case 'firefox': + steps.push({ + command: `.buildkite/scripts/steps/functional/${IS_XPACK ? 'xpack' : 'oss'}_firefox.sh`, + label: `${IS_XPACK ? 'Default' : 'OSS'} Firefox`, + agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, + depends_on: 'build', + parallelism: RUN_COUNT, + concurrency: concurrency, + concurrency_group: UUID, + concurrency_method: 'eager', + }); + break; + + case 'accessibility': + steps.push({ + command: `.buildkite/scripts/steps/functional/${ + IS_XPACK ? 'xpack' : 'oss' + }_accessibility.sh`, + label: `${IS_XPACK ? 'Default' : 'OSS'} Accessibility`, + agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, + depends_on: 'build', + parallelism: RUN_COUNT, + concurrency: concurrency, + concurrency_group: UUID, + concurrency_method: 'eager', + }); + break; } } From 139a3c9866a89b4910e798c0c932a94f3004d0ba Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Nov 2021 11:25:25 -0800 Subject: [PATCH 62/98] [ci] Run Jest tests in parallel (#117188) Signed-off-by: Tyler Smalley --- .buildkite/pipelines/hourly.yml | 15 +++++----- .buildkite/pipelines/pull_request/base.yml | 15 +++++----- .buildkite/scripts/steps/test/jest.sh | 4 +-- .../scripts/steps/test/jest_parallel.sh | 30 +++++++++++++++++++ .eslintrc.js | 1 + jest.config.js | 3 ++ .../kbn-cli-dev-mode/src/dev_server.test.ts | 6 ++-- packages/kbn-rule-data-utils/jest.config.js | 13 -------- .../jest.config.js | 13 -------- .../kbn-securitysolution-rules/jest.config.js | 13 -------- .../jest.config.js | 13 -------- packages/kbn-test/jest-preset.js | 10 ++++++- packages/kbn-test/src/jest/run.ts | 5 ++-- .../src/jest/run_check_jest_configs_cli.ts | 15 ++++++++-- src/plugins/expression_error/jest.config.js | 16 ---------- .../download/ensure_downloaded.test.ts | 3 +- .../server/routes/deprecations.test.ts | 3 +- .../common}/jest.config.js | 11 +++---- .../public/app/jest.config.js | 16 ++++++++++ .../public/cases/jest.config.js | 16 ++++++++++ .../events_viewer/events_viewer.test.tsx | 17 ++++------- .../public/common/jest.config.js | 16 ++++++++++ .../public/detections/jest.config.js | 18 +++++++++++ .../public/hosts/jest.config.js | 16 ++++++++++ .../security_solution/public/jest.config.js | 18 +++++++++++ .../public/management/jest.config.js | 18 +++++++++++ .../public/network/jest.config.js | 16 ++++++++++ .../public/overview/jest.config.js | 16 ++++++++++ .../public/resolver/jest.config.js | 16 ++++++++++ .../{ => public/timelines}/jest.config.js | 9 +++--- .../public/transforms/jest.config.js | 18 +++++++++++ .../server/client/jest.config.js | 16 ++++++++++ .../server/endpoint/jest.config.js | 16 ++++++++++ .../server/fleet_integration/jest.config.js | 18 +++++++++++ .../security_solution/server/jest.config.js | 18 +++++++++++ .../server/lib/jest.config.js | 16 ++++++++++ .../server/search_strategy/jest.config.js | 18 +++++++++++ .../server/usage/jest.config.js | 16 ++++++++++ .../server/utils/jest.config.js | 16 ++++++++++ 39 files changed, 420 insertions(+), 113 deletions(-) create mode 100755 .buildkite/scripts/steps/test/jest_parallel.sh delete mode 100644 packages/kbn-rule-data-utils/jest.config.js delete mode 100644 packages/kbn-securitysolution-list-constants/jest.config.js delete mode 100644 packages/kbn-securitysolution-rules/jest.config.js delete mode 100644 packages/kbn-securitysolution-t-grid/jest.config.js delete mode 100644 src/plugins/expression_error/jest.config.js rename x-pack/plugins/{metrics_entities => security_solution/common}/jest.config.js (53%) create mode 100644 x-pack/plugins/security_solution/public/app/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/cases/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/common/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/detections/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/hosts/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/management/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/network/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/overview/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/resolver/jest.config.js rename x-pack/plugins/security_solution/{ => public/timelines}/jest.config.js (55%) create mode 100644 x-pack/plugins/security_solution/public/transforms/jest.config.js create mode 100644 x-pack/plugins/security_solution/server/client/jest.config.js create mode 100644 x-pack/plugins/security_solution/server/endpoint/jest.config.js create mode 100644 x-pack/plugins/security_solution/server/fleet_integration/jest.config.js create mode 100644 x-pack/plugins/security_solution/server/jest.config.js create mode 100644 x-pack/plugins/security_solution/server/lib/jest.config.js create mode 100644 x-pack/plugins/security_solution/server/search_strategy/jest.config.js create mode 100644 x-pack/plugins/security_solution/server/usage/jest.config.js create mode 100644 x-pack/plugins/security_solution/server/utils/jest.config.js diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 4b2b17d272d17..9e9990816ad1d 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -119,6 +119,14 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/test/jest.sh + label: 'Jest Tests' + parallelism: 8 + agents: + queue: n2-4 + timeout_in_minutes: 90 + key: jest + - command: .buildkite/scripts/steps/test/jest_integration.sh label: 'Jest Integration Tests' agents: @@ -133,13 +141,6 @@ steps: timeout_in_minutes: 120 key: api-integration - - command: .buildkite/scripts/steps/test/jest.sh - label: 'Jest Tests' - agents: - queue: c2-16 - timeout_in_minutes: 120 - key: jest - - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 0f2a4a1026af8..34db52772e619 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -117,6 +117,14 @@ steps: - exit_status: '*' limit: 1 + - command: .buildkite/scripts/steps/test/jest.sh + label: 'Jest Tests' + parallelism: 8 + agents: + queue: n2-4 + timeout_in_minutes: 90 + key: jest + - command: .buildkite/scripts/steps/test/jest_integration.sh label: 'Jest Integration Tests' agents: @@ -131,13 +139,6 @@ steps: timeout_in_minutes: 120 key: api-integration - - command: .buildkite/scripts/steps/test/jest.sh - label: 'Jest Tests' - agents: - queue: c2-16 - timeout_in_minutes: 120 - key: jest - - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: diff --git a/.buildkite/scripts/steps/test/jest.sh b/.buildkite/scripts/steps/test/jest.sh index 2c4e3fe21902d..d2d1ed10043d6 100755 --- a/.buildkite/scripts/steps/test/jest.sh +++ b/.buildkite/scripts/steps/test/jest.sh @@ -9,5 +9,5 @@ is_test_execution_step .buildkite/scripts/bootstrap.sh echo '--- Jest' -checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --ci --verbose --maxWorkers=10 +checks-reporter-with-killswitch "Jest Unit Tests $((BUILDKITE_PARALLEL_JOB+1))" \ + .buildkite/scripts/steps/test/jest_parallel.sh diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh new file mode 100755 index 0000000000000..c9e0e1aff5cf2 --- /dev/null +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -uo pipefail + +JOB=$BUILDKITE_PARALLEL_JOB +JOB_COUNT=$BUILDKITE_PARALLEL_JOB_COUNT + +# a jest failure will result in the script returning an exit code of 10 + +i=0 +exitCode=0 + +while read -r config; do + if [ "$((i % JOB_COUNT))" -eq "$JOB" ]; then + echo "--- $ node scripts/jest --config $config" + node --max-old-space-size=14336 ./node_modules/.bin/jest --config="$config" --runInBand --coverage=false + lastCode=$? + + if [ $lastCode -ne 0 ]; then + exitCode=10 + echo "Jest exited with code $lastCode" + echo "^^^ +++" + fi + fi + + ((i=i+1)) +# uses heredoc to avoid the while loop being in a sub-shell thus unable to overwrite exitCode +done <<< "$(find src x-pack packages -name jest.config.js -not -path "*/__fixtures__/*" | sort)" + +exit $exitCode \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index e338c28f7f848..b303a9fefb691 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -704,6 +704,7 @@ module.exports = { 'packages/kbn-eslint-plugin-eslint/**/*', 'x-pack/gulpfile.js', 'x-pack/scripts/*.js', + '**/jest.config.js', ], excludedFiles: ['**/integration_tests/**/*'], rules: { diff --git a/jest.config.js b/jest.config.js index 09532dc28bbb2..ae07034c10781 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,5 +17,8 @@ module.exports = { '/src/plugins/vis_types/*/jest.config.js', '/test/*/jest.config.js', '/x-pack/plugins/*/jest.config.js', + '/x-pack/plugins/security_solution/*/jest.config.js', + '/x-pack/plugins/security_solution/public/*/jest.config.js', + '/x-pack/plugins/security_solution/server/*/jest.config.js', ], }; diff --git a/packages/kbn-cli-dev-mode/src/dev_server.test.ts b/packages/kbn-cli-dev-mode/src/dev_server.test.ts index 92dbe484eb005..5e386e3de5972 100644 --- a/packages/kbn-cli-dev-mode/src/dev_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.test.ts @@ -79,6 +79,7 @@ expect.addSnapshotSerializer(extendedEnvSerializer); beforeEach(() => { jest.clearAllMocks(); log.messages.length = 0; + process.execArgv = ['--inheritted', '--exec', '--argv']; currentProc = undefined; }); @@ -138,8 +139,9 @@ describe('#run$', () => { "isDevCliChild": "true", }, "nodeOptions": Array [ - "--preserve-symlinks-main", - "--preserve-symlinks", + "--inheritted", + "--exec", + "--argv", ], "stdio": "pipe", }, diff --git a/packages/kbn-rule-data-utils/jest.config.js b/packages/kbn-rule-data-utils/jest.config.js deleted file mode 100644 index 26cb39fe8b55a..0000000000000 --- a/packages/kbn-rule-data-utils/jest.config.js +++ /dev/null @@ -1,13 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/packages/kbn-rule-data-utils'], -}; diff --git a/packages/kbn-securitysolution-list-constants/jest.config.js b/packages/kbn-securitysolution-list-constants/jest.config.js deleted file mode 100644 index 21dffdfcf5a68..0000000000000 --- a/packages/kbn-securitysolution-list-constants/jest.config.js +++ /dev/null @@ -1,13 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/packages/kbn-securitysolution-list-constants'], -}; diff --git a/packages/kbn-securitysolution-rules/jest.config.js b/packages/kbn-securitysolution-rules/jest.config.js deleted file mode 100644 index 99368edd5372c..0000000000000 --- a/packages/kbn-securitysolution-rules/jest.config.js +++ /dev/null @@ -1,13 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/packages/kbn-securitysolution-rules'], -}; diff --git a/packages/kbn-securitysolution-t-grid/jest.config.js b/packages/kbn-securitysolution-t-grid/jest.config.js deleted file mode 100644 index 21e7d2d71b61a..0000000000000 --- a/packages/kbn-securitysolution-t-grid/jest.config.js +++ /dev/null @@ -1,13 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/packages/kbn-securitysolution-t-grid'], -}; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 0199aa6e311b6..db64f070b37d9 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -46,7 +46,15 @@ module.exports = { modulePathIgnorePatterns: ['__fixtures__/', 'target/'], // Use this configuration option to add custom reporters to Jest - reporters: ['default', '@kbn/test/target_node/jest/junit_reporter'], + reporters: [ + 'default', + [ + '@kbn/test/target_node/jest/junit_reporter', + { + rootDirectory: '.', + }, + ], + ], // The paths to modules that run some code to configure or set up the testing environment before each test setupFiles: [ diff --git a/packages/kbn-test/src/jest/run.ts b/packages/kbn-test/src/jest/run.ts index 4a5dd4e9281ba..f2592500beeee 100644 --- a/packages/kbn-test/src/jest/run.ts +++ b/packages/kbn-test/src/jest/run.ts @@ -52,11 +52,12 @@ export function runJest(configName = 'jest.config.js') { const runStartTime = Date.now(); const reportTime = getTimeReporter(log, 'scripts/jest'); - let cwd: string; + let testFiles: string[]; + const cwd: string = process.env.INIT_CWD || process.cwd(); + if (!argv.config) { - cwd = process.env.INIT_CWD || process.cwd(); testFiles = argv._.splice(2).map((p) => resolve(cwd, p)); const commonTestFiles = commonBasePath(testFiles); const testFilesProvided = testFiles.length > 0; diff --git a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts index 5895ef193fbfe..cf37ee82d61e9 100644 --- a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts +++ b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts @@ -26,7 +26,16 @@ const template: string = `module.exports = { }; `; -const roots: string[] = ['x-pack/plugins', 'packages', 'src/plugins', 'test', 'src']; +const roots: string[] = [ + 'x-pack/plugins/security_solution/public', + 'x-pack/plugins/security_solution/server', + 'x-pack/plugins/security_solution', + 'x-pack/plugins', + 'packages', + 'src/plugins', + 'test', + 'src', +]; export async function runCheckJestConfigsCli() { run( @@ -76,7 +85,9 @@ export async function runCheckJestConfigsCli() { modulePath, }); - writeFileSync(resolve(root, name, 'jest.config.js'), content); + const configPath = resolve(root, name, 'jest.config.js'); + log.info('created %s', configPath); + writeFileSync(configPath, content); } else { log.warning(`Unable to determind where to place jest.config.js for ${file}`); } diff --git a/src/plugins/expression_error/jest.config.js b/src/plugins/expression_error/jest.config.js deleted file mode 100644 index 27774f4003f9e..0000000000000 --- a/src/plugins/expression_error/jest.config.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/expression_error'], - coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expression_error', - coverageReporters: ['text', 'html'], - collectCoverageFrom: ['/src/plugins/expression_error/{common,public}/**/*.{ts,tsx}'], -}; diff --git a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts index 955e8214af8fa..9db128c019ac0 100644 --- a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts +++ b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts @@ -17,7 +17,8 @@ import { LevelLogger } from '../../lib'; jest.mock('./checksum'); jest.mock('./download'); -describe('ensureBrowserDownloaded', () => { +// https://github.com/elastic/kibana/issues/115881 +describe.skip('ensureBrowserDownloaded', () => { let logger: jest.Mocked; beforeEach(() => { diff --git a/x-pack/plugins/reporting/server/routes/deprecations.test.ts b/x-pack/plugins/reporting/server/routes/deprecations.test.ts index 5367b6bd531ed..63be2acf52c25 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations.test.ts @@ -24,7 +24,8 @@ import { registerDeprecationsRoutes } from './deprecations'; type SetupServerReturn = UnwrapPromise>; -describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { +// https://github.com/elastic/kibana/issues/115881 +describe.skip(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { const reportingSymbol = Symbol('reporting'); let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; diff --git a/x-pack/plugins/metrics_entities/jest.config.js b/x-pack/plugins/security_solution/common/jest.config.js similarity index 53% rename from x-pack/plugins/metrics_entities/jest.config.js rename to x-pack/plugins/security_solution/common/jest.config.js index 98a391223cc0f..ca6f7cd368f2b 100644 --- a/x-pack/plugins/metrics_entities/jest.config.js +++ b/x-pack/plugins/security_solution/common/jest.config.js @@ -6,10 +6,11 @@ */ module.exports = { - collectCoverageFrom: ['/x-pack/plugins/metrics_entities/{common,server}/**/*.{ts,tsx}'], - coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/metrics_entities', - coverageReporters: ['text', 'html'], preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/plugins/metrics_entities'], + rootDir: '../../../..', + roots: ['/x-pack/plugins/security_solution/common'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/common', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/common/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/security_solution/public/app/jest.config.js b/x-pack/plugins/security_solution/public/app/jest.config.js new file mode 100644 index 0000000000000..452cee5e5b3a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/app'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/app', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/app/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/public/cases/jest.config.js b/x-pack/plugins/security_solution/public/cases/jest.config.js new file mode 100644 index 0000000000000..4b0a49fd65aff --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/cases'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/cases', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/cases/**/*.{ts,tsx}'], +}; 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 a611d140f61d1..cc94f24d04024 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 @@ -184,7 +184,7 @@ describe('EventsViewer', () => { mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]); }); - test('call the right reduce action to show event details', () => { + test('call the right reduce action to show event details', async () => { const wrapper = mount( @@ -195,19 +195,14 @@ describe('EventsViewer', () => { wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); }); - waitFor(() => { - expect(mockDispatch).toBeCalledTimes(2); + await waitFor(() => { + expect(mockDispatch).toBeCalledTimes(3); expect(mockDispatch.mock.calls[1][0]).toEqual({ payload: { - panelView: 'eventDetail', - params: { - eventId: 'yb8TkHYBRgU82_bJu_rY', - indexName: 'auditbeat-7.10.1-2020.12.18-000001', - }, - tabType: 'query', - timelineId: TimelineId.test, + id: 'test', + isLoading: false, }, - type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL', + type: 'x-pack/timelines/t-grid/UPDATE_LOADING', }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/jest.config.js b/x-pack/plugins/security_solution/public/common/jest.config.js new file mode 100644 index 0000000000000..e59f9c68f7590 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/common'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/common', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/common/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/public/detections/jest.config.js b/x-pack/plugins/security_solution/public/detections/jest.config.js new file mode 100644 index 0000000000000..23bc914b6493b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/detections'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/detections', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/detections/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/security_solution/public/hosts/jest.config.js b/x-pack/plugins/security_solution/public/hosts/jest.config.js new file mode 100644 index 0000000000000..b0ead04130b74 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/hosts'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/hosts', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/hosts/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/public/jest.config.js b/x-pack/plugins/security_solution/public/jest.config.js new file mode 100644 index 0000000000000..f2bde770370f4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + /** all nested directories have their own Jest config file */ + testMatch: ['/x-pack/plugins/security_solution/public/*.test.{js,mjs,ts,tsx}'], + roots: ['/x-pack/plugins/security_solution/public'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/public/management/jest.config.js b/x-pack/plugins/security_solution/public/management/jest.config.js new file mode 100644 index 0000000000000..fdb6212ef6c81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/management'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/management/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/security_solution/public/network/jest.config.js b/x-pack/plugins/security_solution/public/network/jest.config.js new file mode 100644 index 0000000000000..6059805c0652a --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/network'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/network', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/network/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/public/overview/jest.config.js b/x-pack/plugins/security_solution/public/overview/jest.config.js new file mode 100644 index 0000000000000..673eece7a36fd --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/overview'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/overview', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/overview/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/public/resolver/jest.config.js b/x-pack/plugins/security_solution/public/resolver/jest.config.js new file mode 100644 index 0000000000000..43e1202d9d8da --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/resolver'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/resolver', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/resolver/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/jest.config.js b/x-pack/plugins/security_solution/public/timelines/jest.config.js similarity index 55% rename from x-pack/plugins/security_solution/jest.config.js rename to x-pack/plugins/security_solution/public/timelines/jest.config.js index 6cfcb65bb5d68..94434b9303d47 100644 --- a/x-pack/plugins/security_solution/jest.config.js +++ b/x-pack/plugins/security_solution/public/timelines/jest.config.js @@ -7,11 +7,12 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/plugins/security_solution'], - coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/security_solution', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/timelines'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/timelines', coverageReporters: ['text', 'html'], collectCoverageFrom: [ - '/x-pack/plugins/security_solution/{common,public,server}/**/*.{ts,tsx}', + '/x-pack/plugins/security_solution/public/timelines/**/*.{ts,tsx}', ], }; diff --git a/x-pack/plugins/security_solution/public/transforms/jest.config.js b/x-pack/plugins/security_solution/public/transforms/jest.config.js new file mode 100644 index 0000000000000..30847fa39a8d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/transforms/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/transforms'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/transforms', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/transforms/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/security_solution/server/client/jest.config.js b/x-pack/plugins/security_solution/server/client/jest.config.js new file mode 100644 index 0000000000000..ba3dd88f83303 --- /dev/null +++ b/x-pack/plugins/security_solution/server/client/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/server/client'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/client', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/server/client/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/jest.config.js b/x-pack/plugins/security_solution/server/endpoint/jest.config.js new file mode 100644 index 0000000000000..4fed1c5e7ac15 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/server/endpoint'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/endpoint', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/server/endpoint/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/jest.config.js b/x-pack/plugins/security_solution/server/fleet_integration/jest.config.js new file mode 100644 index 0000000000000..81625081c40c6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/fleet_integration/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/server/fleet_integration'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/fleet_integration', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/server/fleet_integration/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/security_solution/server/jest.config.js b/x-pack/plugins/security_solution/server/jest.config.js new file mode 100644 index 0000000000000..2fc23670388b9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + /** all nested directories have their own Jest config file */ + testMatch: ['/x-pack/plugins/security_solution/server/*.test.{js,mjs,ts,tsx}'], + roots: ['/x-pack/plugins/security_solution/server'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/server/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/server/lib/jest.config.js b/x-pack/plugins/security_solution/server/lib/jest.config.js new file mode 100644 index 0000000000000..4c4c7d8d4a6b7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/server/lib'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/lib', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/server/lib/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/jest.config.js b/x-pack/plugins/security_solution/server/search_strategy/jest.config.js new file mode 100644 index 0000000000000..93b9ddbf7a27d --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/jest.config.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/server/search_strategy'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/search_strategy', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/server/search_strategy/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/security_solution/server/usage/jest.config.js b/x-pack/plugins/security_solution/server/usage/jest.config.js new file mode 100644 index 0000000000000..82386fea363fe --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/server/usage'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/usage', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/server/usage/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/server/utils/jest.config.js b/x-pack/plugins/security_solution/server/utils/jest.config.js new file mode 100644 index 0000000000000..d3a2e138b789d --- /dev/null +++ b/x-pack/plugins/security_solution/server/utils/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/server/utils'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/server/utils', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/server/utils/**/*.{ts,tsx}'], +}; From 994a4e44f13f29515e0d60c637938723cbd43eae Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Nov 2021 12:10:19 -0800 Subject: [PATCH 63/98] [kbn/io-ts] fix direct import Conflict between https://github.com/elastic/kibana/pull/117958 and https://github.com/elastic/kibana/pull/115145 Signed-off-by: Tyler Smalley --- x-pack/plugins/apm/server/routes/correlations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index 8b20d57d25d67..ac6835fdd6474 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; -import { toNumberRt } from '@kbn/io-ts-utils'; +import { toNumberRt } from '@kbn/io-ts-utils/to_number_rt'; import { isActivePlatinumLicense } from '../../common/license_check'; From d6de4b570baba1f88cfe9224fead405f9320a306 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Nov 2021 12:32:29 -0800 Subject: [PATCH 64/98] [ci] Cleanup Bazel cache config (#118070) Signed-off-by: Tyler Smalley --- src/dev/ci_setup/.bazelrc-ci | 5 ----- src/dev/ci_setup/setup.sh | 11 ----------- src/dev/ci_setup/setup_env.sh | 20 -------------------- 3 files changed, 36 deletions(-) delete mode 100644 src/dev/ci_setup/.bazelrc-ci diff --git a/src/dev/ci_setup/.bazelrc-ci b/src/dev/ci_setup/.bazelrc-ci deleted file mode 100644 index a0a0c3de73405..0000000000000 --- a/src/dev/ci_setup/.bazelrc-ci +++ /dev/null @@ -1,5 +0,0 @@ -# Generated by .buildkite/scripts/common/setup_bazel.sh - -import %workspace%/.bazelrc.common - -build --build_metadata=ROLE=CI diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 62e1b24d6d559..18f11fa7f16e4 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -10,17 +10,6 @@ echo " -- PARENT_DIR='$PARENT_DIR'" echo " -- KIBANA_PKG_BRANCH='$KIBANA_PKG_BRANCH'" echo " -- TEST_ES_SNAPSHOT_VERSION='$TEST_ES_SNAPSHOT_VERSION'" -### -### copy .bazelrc-ci into $HOME/.bazelrc -### -cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; - -### -### append auth token to buildbuddy into "$HOME/.bazelrc"; -### -echo "# Appended by src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" -echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" - ### ### install dependencies ### diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index b9898960135fc..4bbc7235e5cb5 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -181,24 +181,4 @@ if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then export JAVA_HOME=$HOME/.java/$ES_BUILD_JAVA fi -### -### copy .bazelrc-ci into $HOME/.bazelrc -### -cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; - -### -### remove write permissions on buildbuddy remote cache for prs -### -if [[ "$ghprbPullId" ]] ; then - echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" - echo "# Uploads logs & artifacts without writing to cache" >> "$HOME/.bazelrc" - echo "build --noremote_upload_local_results" >> "$HOME/.bazelrc" -fi - -### -### append auth token to buildbuddy into "$HOME/.bazelrc"; -### -echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" -echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" - export CI_ENV_SETUP=true From 7d27afeabd62295ea33a8180391537e903d94153 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Tue, 9 Nov 2021 15:51:24 -0500 Subject: [PATCH 65/98] [Security Solution][Endpoint][Admin] Update empty state description text (#117917) --- .../pages/event_filters/view/components/empty/index.tsx | 2 +- .../pages/host_isolation_exceptions/view/components/empty.tsx | 2 +- .../pages/trusted_apps/view/components/empty_state.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx index 36a7f32ce32dd..df718b9311641 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx @@ -38,7 +38,7 @@ export const EventFiltersListEmptyState = memo<{ body={ } actions={ diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx index 70a30d0890ee4..37922dd776b15 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx @@ -34,7 +34,7 @@ export const HostIsolationExceptionsEmptyState = memo<{ onAdd: () => void }>(({ body={ } actions={ diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx index d64d2fd7f634b..3e35ed3254e47 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx @@ -31,7 +31,7 @@ export const EmptyState = memo<{ body={ } actions={ From 14f6d23922c14cabbe15da434351ca99a9da90a3 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 9 Nov 2021 15:28:00 -0600 Subject: [PATCH 66/98] API test for upstream services and service dependencies (#116722) --- .../dependencies/service_dependencies.spec.ts | 114 ++++++++++++++++++ .../dependencies/upstream_services.spec.ts | 74 ++++++++++++ .../error_groups_main_statistics.spec.ts | 2 +- 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts create mode 100644 x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts diff --git a/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts new file mode 100644 index 0000000000000..4445a6cbc31d3 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/dependencies/service_dependencies.spec.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { BackendNode } from '../../../../plugins/apm/common/connections'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { generateData } from './generate_data'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + const registry = getService('registry'); + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const backendName = 'elasticsearch'; + const serviceName = 'synth-go'; + + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/dependencies', + params: { + path: { serviceName }, + query: { + environment: 'production', + numBuckets: 20, + offset: '1d', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); + } + + registry.when( + 'Dependency for service when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.serviceDependencies).to.empty(); + }); + } + ); + + registry.when( + 'Dependency for services', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + describe('when data is loaded', () => { + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + }); + after(() => synthtraceEsClient.clean()); + + it('returns a list of dependencies for a service', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect( + body.serviceDependencies.map(({ location }) => (location as BackendNode).backendName) + ).to.eql([backendName]); + + const currentStatsLatencyValues = + body.serviceDependencies[0].currentStats.latency.timeseries; + expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); + }); + }); + } + ); + + registry.when( + 'Dependency for service breakdown when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.serviceDependencies).to.empty(); + }); + } + ); + + registry.when( + 'Dependency for services breakdown', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + describe('when data is loaded', () => { + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + }); + after(() => synthtraceEsClient.clean()); + + it('returns a list of dependencies for a service', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect( + body.serviceDependencies.map(({ location }) => (location as BackendNode).backendName) + ).to.eql([backendName]); + + const currentStatsLatencyValues = + body.serviceDependencies[0].currentStats.latency.timeseries; + expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); + }); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.ts new file mode 100644 index 0000000000000..0c730ebfb53ad --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/dependencies/upstream_services.spec.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 expect from '@kbn/expect'; +import { ServiceNode } from '../../../../plugins/apm/common/connections'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { generateData } from './generate_data'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + const registry = getService('registry'); + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const backendName = 'elasticsearch'; + + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/backends/upstream_services', + params: { + query: { + backendName, + environment: 'production', + kuery: '', + numBuckets: 20, + offset: '1d', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); + } + + registry.when( + 'Dependency upstream services when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.services).to.empty(); + }); + } + ); + + registry.when( + 'Dependency upstream services', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + describe('when data is loaded', () => { + before(async () => { + await generateData({ synthtraceEsClient, start, end }); + }); + after(() => synthtraceEsClient.clean()); + + it('returns a list of upstream services for the dependency', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + expect(body.services.map(({ location }) => (location as ServiceNode).serviceName)).to.eql( + ['synth-go'] + ); + + const currentStatsLatencyValues = body.services[0].currentStats.latency.timeseries; + expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true); + }); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts index 3d8ddfe38cf5e..5774ce4225f5a 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups/error_groups_main_statistics.spec.ts @@ -80,7 +80,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { errorGroupMainStatistics = response.body; }); - it('returns correct number of occurrencies', () => { + it('returns correct number of occurrences', () => { expect(errorGroupMainStatistics.error_groups.length).to.equal(2); expect(errorGroupMainStatistics.error_groups.map((error) => error.name).sort()).to.eql([ ERROR_NAME_1, From 21ba17d8011f7e2e1fe17c265c3218b2757b8694 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Nov 2021 13:56:06 -0800 Subject: [PATCH 67/98] [docs] Use inspect-brk instead of debug-brk (#118108) Signed-off-by: Tyler Smalley --- .../contributing/development-functional-tests.asciidoc | 2 +- docs/developer/contributing/development-unit-tests.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer/contributing/development-functional-tests.asciidoc b/docs/developer/contributing/development-functional-tests.asciidoc index cb614c5149f95..4695a499ca6b6 100644 --- a/docs/developer/contributing/development-functional-tests.asciidoc +++ b/docs/developer/contributing/development-functional-tests.asciidoc @@ -490,7 +490,7 @@ From the command line run: ["source","shell"] ----------- -node --debug-brk --inspect scripts/functional_test_runner +node --inspect-brk scripts/functional_test_runner ----------- This prints out a URL that you can visit in Chrome and debug your functional tests in the browser. diff --git a/docs/developer/contributing/development-unit-tests.asciidoc b/docs/developer/contributing/development-unit-tests.asciidoc index 9f0896f8a673f..0a21dbbb449cc 100644 --- a/docs/developer/contributing/development-unit-tests.asciidoc +++ b/docs/developer/contributing/development-unit-tests.asciidoc @@ -75,7 +75,7 @@ In order to ease the pain specialized tasks provide alternate methods for running the tests. You could also add the `--debug` option so that `node` is run using -the `--debug-brk` flag. You’ll need to connect a remote debugger such +the `--inspect-brk` flag. You’ll need to connect a remote debugger such as https://github.com/node-inspector/node-inspector[`node-inspector`] to proceed in this mode. From 5ffec0bd2ce56761ee42eac27d9ee3110f5a6aa2 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Nov 2021 14:17:23 -0800 Subject: [PATCH 68/98] Skip failing test Signed-off-by: Tyler Smalley --- packages/kbn-cli-dev-mode/src/dev_server.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-cli-dev-mode/src/dev_server.test.ts b/packages/kbn-cli-dev-mode/src/dev_server.test.ts index 5e386e3de5972..7c661bd484778 100644 --- a/packages/kbn-cli-dev-mode/src/dev_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.test.ts @@ -117,7 +117,7 @@ afterEach(() => { subscriptions.length = 0; }); -describe('#run$', () => { +describe.skip('#run$', () => { it('starts the dev server with the right options', () => { run(new DevServer(defaultOptions)).unsubscribe(); From ed19a9c1a9960a8b5a6d37db3ea00c567673c5f5 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 9 Nov 2021 16:31:01 -0700 Subject: [PATCH 69/98] [data.search] Remove warning toast (#117252) * [data.search] Remove toast notification for warnings * Update docs * Review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/data/common/search/types.ts | 2 +- .../data/public/search/fetch/handle_response.tsx | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 68a25d4c4d69d..e9b6160c4a75a 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -71,7 +71,7 @@ export interface IKibanaSearchResponse { isRestored?: boolean; /** - * Optional warnings that should be surfaced to the end user + * Optional warnings returned from Elasticsearch (for example, deprecation warnings) */ warning?: string; diff --git a/src/plugins/data/public/search/fetch/handle_response.tsx b/src/plugins/data/public/search/fetch/handle_response.tsx index f4acebfb36060..9e68209af2b92 100644 --- a/src/plugins/data/public/search/fetch/handle_response.tsx +++ b/src/plugins/data/public/search/fetch/handle_response.tsx @@ -16,17 +16,7 @@ import { getNotifications } from '../../services'; import { SearchRequest } from '..'; export function handleResponse(request: SearchRequest, response: IKibanaSearchResponse) { - const { rawResponse, warning } = response; - if (warning) { - getNotifications().toasts.addWarning({ - title: i18n.translate('data.search.searchSource.fetch.warningMessage', { - defaultMessage: 'Warning: {warning}', - values: { - warning, - }, - }), - }); - } + const { rawResponse } = response; if (rawResponse.timed_out) { getNotifications().toasts.addWarning({ From eb9c8692aab7e0e21550d71b8288cbfa3e162980 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 9 Nov 2021 19:12:35 -0600 Subject: [PATCH 70/98] [dev docs] Add WSL setup guide (#109670) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../getting_started/development_windows.mdx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 dev_docs/getting_started/development_windows.mdx diff --git a/dev_docs/getting_started/development_windows.mdx b/dev_docs/getting_started/development_windows.mdx new file mode 100644 index 0000000000000..4300c307a7b11 --- /dev/null +++ b/dev_docs/getting_started/development_windows.mdx @@ -0,0 +1,45 @@ +--- +id: kibDevTutorialSetupDevWindows +slug: /kibana-dev-docs/tutorial/setup-dev-windows +title: Development on Windows +summary: Learn how to setup a development environment on Windows +date: 2021-08-11 +tags: ['kibana', 'onboarding', 'dev', 'windows', 'setup'] +--- + + +# Overview + +Development on Windows is recommended through WSL2. WSL lets users run a Linux environment on Windows, providing a supported development environment for Kibana. + +## Install WSL + +The latest setup instructions can be found at https://docs.microsoft.com/en-us/windows/wsl/install-win10 + +1) Open Powershell as an administrator +1) Enable WSL + ``` + dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart + ``` +1) Enable Virtual Machine Platform + ``` + dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart + ``` +1) Download and install the [Linux kernel update package](https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi) +1) Set WSL 2 as the default version + ``` + wsl --set-default-version 2 + ``` +1) Open the Micrsoft Store application and install a Linux distribution + +## Setup Kibana + +1. + +## Install VS Code + +Remote development is supported with an extension. [Reference](https://code.visualstudio.com/docs/remote/wsl). + +1) Install VS Code on Windows +1) Check the "Add to PATH" option during setup +1) Install the [Remote Development](https://aka.ms/vscode-remote/download/extension) package From 9e36356f9d463a55ff648dac65a2576f1a758842 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 10 Nov 2021 02:24:01 +0000 Subject: [PATCH 71/98] skip flaky suite (#118023) --- .../apm_api_integration/tests/correlations/latency.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts index 5d73a6a0499b0..6be2399729339 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts @@ -105,7 +105,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'trial', archives: ['8.0.0'] }, () => { // putting this into a single `it` because the responses depend on each other - it('runs queries and returns results', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/118023 + it.skip('runs queries and returns results', async () => { const overallDistributionResponse = await apmApiClient.readUser({ endpoint: 'POST /internal/apm/latency/overall_distribution', params: { From 90ea8f8e4cfce03ce2ea62cdd9fdca5b682d79ed Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 9 Nov 2021 21:13:13 -0600 Subject: [PATCH 72/98] [SecuritySolution] Fixes deprecated use of Spaces API (#118121) * Set up our app client in start() instead of setup() We don't use it during setup, and since the spaces client is now only available in start(), we need to respond accordingly. * Move appClientFactory setup back into the setup phase Albeit in the getStartServices callback, so it's really in the start phase. * Use spaces service from start contract in RequestContextFactory The setup contract variants have been deprecated. * Remove unused dependency from EndpointAppContextService It no longer needs the appClientFactory, as that functionality is provided through securitySolutionRequestContextFactory, which wraps an instance of that client itself. --- .../endpoint/endpoint_app_context_services.ts | 2 -- .../security_solution/server/endpoint/mocks.ts | 4 ---- x-pack/plugins/security_solution/server/plugin.ts | 15 +++++++-------- .../security_solution/server/plugin_contract.ts | 3 ++- .../server/request_context_factory.ts | 14 ++++++-------- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index caea18da75ae4..d06739d9b859a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -25,7 +25,6 @@ import { getPackagePolicyDeleteCallback, } from '../fleet_integration/fleet_integration'; import { ManifestManager } from './services/artifacts'; -import { AppClientFactory } from '../client'; import { ConfigType } from '../config'; import { IRequestContextFactory } from '../request_context_factory'; import { LicenseService } from '../../common/license'; @@ -49,7 +48,6 @@ export type EndpointAppContextServiceStartContract = Partial< logger: Logger; endpointMetadataService: EndpointMetadataService; manifestManager?: ManifestManager; - appClientFactory: AppClientFactory; security: SecurityPluginStart; alerting: AlertsPluginStartContract; config: ConfigType; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 190770f3d860d..6c2df2d09f6d5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -18,7 +18,6 @@ import { createMockAgentService, createArtifactsClientMock, } from '../../../fleet/server/mocks'; -import { AppClientFactory } from '../client'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService, @@ -87,8 +86,6 @@ export const createMockEndpointAppContextServiceSetupContract = export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked => { const config = createMockConfig(); - const factory = new AppClientFactory(); - factory.setup({ getSpaceId: () => 'mockSpace', config }); const casesClientMock = createCasesClientMock(); const savedObjectsStart = savedObjectsServiceMock.createStartContract(); @@ -107,7 +104,6 @@ export const createMockEndpointAppContextServiceStartContract = packageService: createMockPackageService(), logger: loggingSystemMock.create().get('mock_endpoint_app_context'), manifestManager: getManifestManagerMock(), - appClientFactory: factory, security: securityMock.createStart(), alerting: alertsMock.createStart(), config, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 3c281d8384628..843bd0ed7019d 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -130,15 +130,10 @@ export class Plugin implements ISecuritySolutionPlugin { ): SecuritySolutionPluginSetup { this.logger.debug('plugin setup'); - const { pluginContext, config, logger, appClientFactory } = this; + const { appClientFactory, pluginContext, config, logger } = this; const experimentalFeatures = config.experimentalFeatures; this.kibanaIndex = core.savedObjects.getKibanaIndex(); - appClientFactory.setup({ - getSpaceId: plugins.spaces?.spacesService?.getSpaceId, - config, - }); - initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings, experimentalFeatures); @@ -308,6 +303,11 @@ export class Plugin implements ISecuritySolutionPlugin { } core.getStartServices().then(([_, depsStart]) => { + appClientFactory.setup({ + getSpaceId: depsStart.spaces?.spacesService?.getSpaceId, + config, + }); + const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider( depsStart.data, endpointContext @@ -339,7 +339,7 @@ export class Plugin implements ISecuritySolutionPlugin { core: SecuritySolutionPluginCoreStartDependencies, plugins: SecuritySolutionPluginStartDependencies ): SecuritySolutionPluginStart { - const { config, logger, appClientFactory } = this; + const { config, logger } = this; const savedObjectsClient = new SavedObjectsClient(core.savedObjects.createInternalRepository()); const registerIngestCallback = plugins.fleet?.registerExternalCallback; @@ -407,7 +407,6 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.fleet?.agentPolicyService!, logger ), - appClientFactory, security: plugins.security, alerting: plugins.alerting, config: this.config, diff --git a/x-pack/plugins/security_solution/server/plugin_contract.ts b/x-pack/plugins/security_solution/server/plugin_contract.ts index 0d8666ff169cd..2566e0ceb5089 100644 --- a/x-pack/plugins/security_solution/server/plugin_contract.ts +++ b/x-pack/plugins/security_solution/server/plugin_contract.ts @@ -33,7 +33,7 @@ import { RuleRegistryPluginStartContract as RuleRegistryPluginStart, } from '../../rule_registry/server'; import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; -import { SpacesPluginSetup } from '../../spaces/server'; +import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import { TaskManagerSetupContract as TaskManagerPluginSetup, TaskManagerStartContract as TaskManagerPluginStart, @@ -68,6 +68,7 @@ export interface SecuritySolutionPluginStartDependencies { licensing: LicensingPluginStart; ruleRegistry: RuleRegistryPluginStart; security: SecurityPluginStart; + spaces?: SpacesPluginStart; taskManager?: TaskManagerPluginStart; telemetry?: TelemetryPluginStart; } diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 0028d624c2955..b7586ee959652 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -36,13 +36,7 @@ export class RequestContextFactory implements IRequestContextFactory { private readonly appClientFactory: AppClientFactory; constructor(private readonly options: ConstructorOptions) { - const { config, plugins } = options; - this.appClientFactory = new AppClientFactory(); - this.appClientFactory.setup({ - getSpaceId: plugins.spaces?.spacesService?.getSpaceId, - config, - }); } public async create( @@ -51,10 +45,14 @@ export class RequestContextFactory implements IRequestContextFactory { ): Promise { const { options, appClientFactory } = this; const { config, core, plugins } = options; - const { lists, ruleRegistry, security, spaces } = plugins; + const { lists, ruleRegistry, security } = plugins; const [, startPlugins] = await core.getStartServices(); const frameworkRequest = await buildFrameworkRequest(context, security, request); + appClientFactory.setup({ + getSpaceId: startPlugins.spaces?.spacesService?.getSpaceId, + config, + }); return { core: context.core, @@ -65,7 +63,7 @@ export class RequestContextFactory implements IRequestContextFactory { getAppClient: () => appClientFactory.create(request), - getSpaceId: () => spaces?.spacesService?.getSpaceId(request) || DEFAULT_SPACE_ID, + getSpaceId: () => startPlugins.spaces?.spacesService?.getSpaceId(request) || DEFAULT_SPACE_ID, getRuleDataService: () => ruleRegistry.ruleDataService, From ad1e2ad00bbf4be006473f0b76b6532815481a38 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 9 Nov 2021 20:31:43 -0700 Subject: [PATCH 73/98] Exponentially backs off retry attempts for sending usage data (#117955) --- .../public/services/telemetry_sender.test.ts | 292 +++++++++++------- .../public/services/telemetry_sender.ts | 37 ++- 2 files changed, 214 insertions(+), 115 deletions(-) diff --git a/src/plugins/telemetry/public/services/telemetry_sender.test.ts b/src/plugins/telemetry/public/services/telemetry_sender.test.ts index 10da46fe2761d..d4678ce0ea23a 100644 --- a/src/plugins/telemetry/public/services/telemetry_sender.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_sender.test.ts @@ -127,67 +127,111 @@ describe('TelemetrySender', () => { expect(telemetryService.getIsOptedIn).toBeCalledTimes(0); expect(shouldSendReport).toBe(false); }); + }); + describe('sendIfDue', () => { + let originalFetch: typeof window['fetch']; + let mockFetch: jest.Mock; - describe('sendIfDue', () => { - let originalFetch: typeof window['fetch']; - let mockFetch: jest.Mock; + beforeAll(() => { + originalFetch = window.fetch; + }); - beforeAll(() => { - originalFetch = window.fetch; - }); + beforeEach(() => (window.fetch = mockFetch = jest.fn())); + afterAll(() => (window.fetch = originalFetch)); - beforeEach(() => (window.fetch = mockFetch = jest.fn())); - afterAll(() => (window.fetch = originalFetch)); + it('does not send if shouldSendReport returns false', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(false); + telemetrySender['retryCount'] = 0; + await telemetrySender['sendIfDue'](); - it('does not send if already sending', async () => { - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetrySender['shouldSendReport'] = jest.fn(); - telemetrySender['isSending'] = true; - await telemetrySender['sendIfDue'](); + expect(telemetrySender['shouldSendReport']).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(0); + }); - expect(telemetrySender['shouldSendReport']).toBeCalledTimes(0); - expect(mockFetch).toBeCalledTimes(0); - }); + it('does not send if we are in screenshot mode', async () => { + const telemetryService = mockTelemetryService({ isScreenshotMode: true }); + const telemetrySender = new TelemetrySender(telemetryService); + await telemetrySender['sendIfDue'](); - it('does not send if shouldSendReport returns false', async () => { - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(false); - telemetrySender['isSending'] = false; - await telemetrySender['sendIfDue'](); + expect(mockFetch).toBeCalledTimes(0); + }); - expect(telemetrySender['shouldSendReport']).toBeCalledTimes(1); - expect(mockFetch).toBeCalledTimes(0); - }); + it('updates last lastReported and calls saveToBrowser', async () => { + const lastReported = Date.now() - (REPORT_INTERVAL_MS + 1000); + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['sendUsageData'] = jest.fn().mockReturnValue(true); + telemetrySender['saveToBrowser'] = jest.fn(); + telemetrySender['lastReported'] = `${lastReported}`; - it('does not send if we are in screenshot mode', async () => { - const telemetryService = mockTelemetryService({ isScreenshotMode: true }); - const telemetrySender = new TelemetrySender(telemetryService); - telemetrySender['isSending'] = false; - await telemetrySender['sendIfDue'](); + await telemetrySender['sendIfDue'](); - expect(mockFetch).toBeCalledTimes(0); - }); + expect(telemetrySender['lastReported']).not.toBe(lastReported); + expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1); + expect(telemetrySender['retryCount']).toEqual(0); + expect(telemetrySender['sendUsageData']).toHaveBeenCalledTimes(1); + }); + + it('resets the retry counter when report is due', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['sendUsageData'] = jest.fn(); + telemetrySender['saveToBrowser'] = jest.fn(); + telemetrySender['retryCount'] = 9; + + await telemetrySender['sendIfDue'](); + expect(telemetrySender['retryCount']).toEqual(0); + expect(telemetrySender['sendUsageData']).toHaveBeenCalledTimes(1); + }); + }); + + describe('sendUsageData', () => { + let originalFetch: typeof window['fetch']; + let mockFetch: jest.Mock; + let consoleWarnMock: jest.SpyInstance; + + beforeAll(() => { + originalFetch = window.fetch; + }); - it('sends report if due', async () => { - const mockClusterUuid = 'mk_uuid'; - const mockTelemetryUrl = 'telemetry_cluster_url'; - const mockTelemetryPayload = [ - { clusterUuid: mockClusterUuid, stats: 'hashed_cluster_usage_data1' }, - ]; - - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); - telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); - telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); - telemetrySender['isSending'] = false; - await telemetrySender['sendIfDue'](); - - expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); - expect(mockFetch).toBeCalledTimes(1); - expect(mockFetch.mock.calls[0]).toMatchInlineSnapshot(` + beforeEach(() => { + window.fetch = mockFetch = jest.fn(); + jest.useFakeTimers(); + consoleWarnMock = jest.spyOn(global.console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + window.fetch = originalFetch; + jest.useRealTimers(); + }); + + it('sends the report', async () => { + const mockClusterUuid = 'mk_uuid'; + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = [ + { clusterUuid: mockClusterUuid, stats: 'hashed_cluster_usage_data1' }, + ]; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + + await telemetrySender['sendUsageData'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(1); + expect(mockFetch.mock.calls[0]).toMatchInlineSnapshot(` Array [ "telemetry_cluster_url", Object { @@ -202,73 +246,113 @@ describe('TelemetrySender', () => { }, ] `); - }); + }); - it('sends report separately for every cluster', async () => { - const mockTelemetryUrl = 'telemetry_cluster_url'; - const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + it('sends report separately for every cluster', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); - telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); - telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); - telemetrySender['isSending'] = false; - await telemetrySender['sendIfDue'](); + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + await telemetrySender['sendIfDue'](); - expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); - expect(mockFetch).toBeCalledTimes(2); - }); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + }); - it('updates last lastReported and calls saveToBrowser', async () => { - const mockTelemetryUrl = 'telemetry_cluster_url'; - const mockTelemetryPayload = ['hashed_cluster_usage_data1']; + it('does not increase the retry counter on successful send', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1']; - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); - telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); - telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); - telemetrySender['saveToBrowser'] = jest.fn(); + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['saveToBrowser'] = jest.fn(); - await telemetrySender['sendIfDue'](); + await telemetrySender['sendUsageData'](); + + expect(mockFetch).toBeCalledTimes(1); + expect(telemetrySender['retryCount']).toBe(0); + }); - expect(mockFetch).toBeCalledTimes(1); - expect(telemetrySender['lastReported']).toBeDefined(); - expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1); - expect(telemetrySender['isSending']).toBe(false); + it('catches fetchTelemetry errors and retries again', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { + throw Error('Error fetching usage'); }); + await telemetrySender['sendUsageData'](); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(telemetrySender['retryCount']).toBe(1); + expect(setTimeout).toBeCalledTimes(1); + expect(setTimeout).toBeCalledWith(telemetrySender['sendUsageData'], 120000); + expect(consoleWarnMock).not.toBeCalled(); // console.warn is only triggered when the retryCount exceeds the allowed number + }); - it('catches fetchTelemetry errors and sets isSending to false', async () => { - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn(); - telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { - throw Error('Error fetching usage'); - }); - await telemetrySender['sendIfDue'](); - expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); - expect(telemetrySender['lastReported']).toBeUndefined(); - expect(telemetrySender['isSending']).toBe(false); + it('catches fetch errors and sets a new timeout if fetch fails more than once', async () => { + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + mockFetch.mockImplementation(() => { + throw Error('Error sending usage'); }); + telemetrySender['retryCount'] = 3; + await telemetrySender['sendUsageData'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + expect(telemetrySender['retryCount']).toBe(4); + expect(setTimeout).toBeCalledWith(telemetrySender['sendUsageData'], 960000); + + await telemetrySender['sendUsageData'](); + expect(telemetrySender['retryCount']).toBe(5); + expect(setTimeout).toBeCalledWith(telemetrySender['sendUsageData'], 1920000); + expect(consoleWarnMock).not.toBeCalled(); // console.warn is only triggered when the retryCount exceeds the allowed number + }); - it('catches fetch errors and sets isSending to false', async () => { - const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; - const telemetryService = mockTelemetryService(); - const telemetrySender = new TelemetrySender(telemetryService); - telemetryService.getTelemetryUrl = jest.fn(); - telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); - mockFetch.mockImplementation(() => { - throw Error('Error sending usage'); - }); - await telemetrySender['sendIfDue'](); - expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); - expect(mockFetch).toBeCalledTimes(2); - expect(telemetrySender['lastReported']).toBeUndefined(); - expect(telemetrySender['isSending']).toBe(false); + it('stops trying to resend the data after 20 retries', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { + throw Error('Error fetching usage'); }); + telemetrySender['retryCount'] = 21; + await telemetrySender['sendUsageData'](); + expect(setTimeout).not.toBeCalled(); + expect(consoleWarnMock.mock.calls[0][0]).toBe( + 'TelemetrySender.sendUsageData exceeds number of retry attempts with Error fetching usage' + ); + }); + }); + + describe('getRetryDelay', () => { + beforeEach(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + it('sets a minimum retry delay of 60 seconds', () => { + expect(TelemetrySender.getRetryDelay(0)).toBe(60000); + }); + + it('changes the retry delay depending on the retry count', () => { + expect(TelemetrySender.getRetryDelay(3)).toBe(480000); + expect(TelemetrySender.getRetryDelay(5)).toBe(1920000); + }); + + it('sets a maximum retry delay of 64 min', () => { + expect(TelemetrySender.getRetryDelay(8)).toBe(3840000); + expect(TelemetrySender.getRetryDelay(10)).toBe(3840000); }); }); + describe('startChecking', () => { beforeEach(() => jest.useFakeTimers()); afterAll(() => jest.useRealTimers()); diff --git a/src/plugins/telemetry/public/services/telemetry_sender.ts b/src/plugins/telemetry/public/services/telemetry_sender.ts index 87287a420e725..fb87b0b23ad56 100644 --- a/src/plugins/telemetry/public/services/telemetry_sender.ts +++ b/src/plugins/telemetry/public/services/telemetry_sender.ts @@ -17,10 +17,14 @@ import type { EncryptedTelemetryPayload } from '../../common/types'; export class TelemetrySender { private readonly telemetryService: TelemetryService; - private isSending: boolean = false; private lastReported?: string; private readonly storage: Storage; - private intervalId?: number; + private intervalId: number = 0; // setInterval returns a positive integer, 0 means no interval is set + private retryCount: number = 0; + + static getRetryDelay(retryCount: number) { + return 60 * (1000 * Math.min(Math.pow(2, retryCount), 64)); // 120s, 240s, 480s, 960s, 1920s, 3840s, 3840s, 3840s + } constructor(telemetryService: TelemetryService) { this.telemetryService = telemetryService; @@ -54,12 +58,17 @@ export class TelemetrySender { }; private sendIfDue = async (): Promise => { - if (this.isSending || !this.shouldSendReport()) { + if (!this.shouldSendReport()) { return; } + // optimistically update the report date and reset the retry counter for a new time report interval window + this.lastReported = `${Date.now()}`; + this.saveToBrowser(); + this.retryCount = 0; + await this.sendUsageData(); + }; - // mark that we are working so future requests are ignored until we're done - this.isSending = true; + private sendUsageData = async (): Promise => { try { const telemetryUrl = this.telemetryService.getTelemetryUrl(); const telemetryPayload: EncryptedTelemetryPayload = @@ -80,17 +89,23 @@ export class TelemetrySender { }) ) ); - this.lastReported = `${Date.now()}`; - this.saveToBrowser(); } catch (err) { - // ignore err - } finally { - this.isSending = false; + // ignore err and try again but after a longer wait period. + this.retryCount = this.retryCount + 1; + if (this.retryCount < 20) { + // exponentially backoff the time between subsequent retries to up to 19 attempts, after which we give up until the next report is due + window.setTimeout(this.sendUsageData, TelemetrySender.getRetryDelay(this.retryCount)); + } else { + /* eslint no-console: ["error", { allow: ["warn"] }] */ + console.warn( + `TelemetrySender.sendUsageData exceeds number of retry attempts with ${err.message}` + ); + } } }; public startChecking = () => { - if (typeof this.intervalId === 'undefined') { + if (this.intervalId === 0) { this.intervalId = window.setInterval(this.sendIfDue, 60000); } }; From 92ffb9e09a2fcd412545d8f160f2aa947c641e24 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Nov 2021 20:27:04 -0800 Subject: [PATCH 74/98] [jest] Fix snapshot caused by environment (#118114) Signed-off-by: Tyler Smalley --- packages/kbn-cli-dev-mode/src/dev_server.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/kbn-cli-dev-mode/src/dev_server.test.ts b/packages/kbn-cli-dev-mode/src/dev_server.test.ts index 7c661bd484778..772ba097fc31a 100644 --- a/packages/kbn-cli-dev-mode/src/dev_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.test.ts @@ -80,6 +80,7 @@ beforeEach(() => { jest.clearAllMocks(); log.messages.length = 0; process.execArgv = ['--inheritted', '--exec', '--argv']; + process.env.FORCE_COLOR = process.env.FORCE_COLOR || '1'; currentProc = undefined; }); @@ -117,13 +118,10 @@ afterEach(() => { subscriptions.length = 0; }); -describe.skip('#run$', () => { +describe('#run$', () => { it('starts the dev server with the right options', () => { run(new DevServer(defaultOptions)).unsubscribe(); - // ensure that FORCE_COLOR is in the env for consistency in snapshot - process.env.FORCE_COLOR = process.env.FORCE_COLOR || 'true'; - expect(execa.node.mock.calls).toMatchInlineSnapshot(` Array [ Array [ @@ -136,6 +134,7 @@ describe.skip('#run$', () => { "env": Object { "": true, "ELASTIC_APM_SERVICE_NAME": "kibana", + "FORCE_COLOR": "true", "isDevCliChild": "true", }, "nodeOptions": Array [ From 24477e9af9721b5df3b51f8d0df6f07f48371e24 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 9 Nov 2021 21:53:43 -0700 Subject: [PATCH 75/98] [Reporting] Allow POST URL in Canvas to be copied (#117954) --- .../workpad_header/share_menu/share_menu.tsx | 1 + .../screen_capture_panel_content.test.tsx | 37 +++++++++++++++++++ .../public/shared/get_shared_components.tsx | 3 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx index 50a3890673ffa..c66336a9153c0 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx @@ -49,6 +49,7 @@ export const ShareMenu = () => { getJobParams={() => getPdfJobParams(sharingData, platformService.getKibanaVersion())} layoutOption="canvas" onClose={onClose} + objectId={workpad.id} /> ) : null; diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx index f37aaea114cfa..0366c1c6d052c 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx @@ -63,6 +63,43 @@ test('ScreenCapturePanelContent properly renders a view with "canvas" layout opt expect(component.text()).toMatch('Full page layout'); }); +test('ScreenCapturePanelContent allows POST URL to be copied when objectId is provided', () => { + const component = mount( + + + + ); + expect(component.text()).toMatch('Copy POST URL'); + expect(component.text()).not.toMatch('Unsaved work'); +}); + +test('ScreenCapturePanelContent does not allow POST URL to be copied when objectId is not provided', () => { + const component = mount( + + + + ); + expect(component.text()).not.toMatch('Copy POST URL'); + expect(component.text()).toMatch('Unsaved work'); +}); + test('ScreenCapturePanelContent properly renders a view with "print" layout option', () => { const component = mount( 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 623e06dd74462..b08036e8b1c80 100644 --- a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx +++ b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx @@ -16,7 +16,8 @@ interface IncludeOnCloseFn { onClose: () => void; } -type Props = Pick & IncludeOnCloseFn; +type Props = Pick & + IncludeOnCloseFn; /* * As of 7.14, the only shared component is a PDF report that is suited for Canvas integration. From a861170c47fbf21adf35b3b3b66d6151a27247c0 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 10 Nov 2021 06:12:53 +0100 Subject: [PATCH 76/98] Move audit logs to a dedicated logs directory (#116562) Co-authored-by: Aleh Zasypkin --- docs/settings/security-settings.asciidoc | 2 +- logs/.empty | 0 packages/kbn-utils/src/path/index.test.ts | 28 ++++++++++++++----- packages/kbn-utils/src/path/index.ts | 8 ++++++ src/dev/build/tasks/clean_tasks.ts | 1 + .../tasks/create_empty_dirs_and_files_task.ts | 6 +++- src/dev/build/tasks/os_packages/run_fpm.ts | 5 ++++ x-pack/plugins/security/server/config.test.ts | 4 +-- x-pack/plugins/security/server/config.ts | 4 +-- 9 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 logs/.empty diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 2ed3c21c482d5..56d08ee24efe1 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -332,7 +332,7 @@ For more details and a reference of audit events, refer to < type: rolling-file - fileName: ./data/audit.log + fileName: ./logs/audit.log policy: type: time-interval interval: 24h <2> diff --git a/logs/.empty b/logs/.empty new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/kbn-utils/src/path/index.test.ts b/packages/kbn-utils/src/path/index.test.ts index daa2cb8dc9a5d..307d47af9ac50 100644 --- a/packages/kbn-utils/src/path/index.test.ts +++ b/packages/kbn-utils/src/path/index.test.ts @@ -7,21 +7,35 @@ */ import { accessSync, constants } from 'fs'; -import { getConfigPath, getDataPath, getConfigDirectory } from './'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { getConfigPath, getDataPath, getLogsPath, getConfigDirectory } from './'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); describe('Default path finder', () => { - it('should find a kibana.yml', () => { - const configPath = getConfigPath(); - expect(() => accessSync(configPath, constants.R_OK)).not.toThrow(); + it('should expose a path to the config directory', () => { + expect(getConfigDirectory()).toMatchInlineSnapshot('/config'); }); - it('should find a data directory', () => { - const dataPath = getDataPath(); - expect(() => accessSync(dataPath, constants.R_OK)).not.toThrow(); + it('should expose a path to the kibana.yml', () => { + expect(getConfigPath()).toMatchInlineSnapshot('/config/kibana.yml'); + }); + + it('should expose a path to the data directory', () => { + expect(getDataPath()).toMatchInlineSnapshot('/data'); + }); + + it('should expose a path to the logs directory', () => { + expect(getLogsPath()).toMatchInlineSnapshot('/logs'); }); it('should find a config directory', () => { const configDirectory = getConfigDirectory(); expect(() => accessSync(configDirectory, constants.R_OK)).not.toThrow(); }); + + it('should find a kibana.yml', () => { + const configPath = getConfigPath(); + expect(() => accessSync(configPath, constants.R_OK)).not.toThrow(); + }); }); diff --git a/packages/kbn-utils/src/path/index.ts b/packages/kbn-utils/src/path/index.ts index 15d6a3eddf01e..c839522441c7c 100644 --- a/packages/kbn-utils/src/path/index.ts +++ b/packages/kbn-utils/src/path/index.ts @@ -27,6 +27,8 @@ const CONFIG_DIRECTORIES = [ const DATA_PATHS = [join(REPO_ROOT, 'data'), '/var/lib/kibana'].filter(isString); +const LOGS_PATHS = [join(REPO_ROOT, 'logs'), '/var/log/kibana'].filter(isString); + function findFile(paths: string[]) { const availablePath = paths.find((configPath) => { try { @@ -57,6 +59,12 @@ export const getConfigDirectory = () => findFile(CONFIG_DIRECTORIES); */ export const getDataPath = () => findFile(DATA_PATHS); +/** + * Get the directory containing logs + * @internal + */ +export const getLogsPath = () => findFile(LOGS_PATHS); + export type PathConfigType = TypeOf; export const config = { diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index f9fcbc74b0efc..19747ce72b5a6 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -196,6 +196,7 @@ export const CleanEmptyFolders: Task = { await deleteEmptyFolders(log, build.resolvePath('.'), [ build.resolvePath('plugins'), build.resolvePath('data'), + build.resolvePath('logs'), ]); }, }; diff --git a/src/dev/build/tasks/create_empty_dirs_and_files_task.ts b/src/dev/build/tasks/create_empty_dirs_and_files_task.ts index 26ed25e801475..dd4cea350ba00 100644 --- a/src/dev/build/tasks/create_empty_dirs_and_files_task.ts +++ b/src/dev/build/tasks/create_empty_dirs_and_files_task.ts @@ -12,6 +12,10 @@ export const CreateEmptyDirsAndFiles: Task = { description: 'Creating some empty directories and files to prevent file-permission issues', async run(config, log, build) { - await Promise.all([mkdirp(build.resolvePath('plugins')), mkdirp(build.resolvePath('data'))]); + await Promise.all([ + mkdirp(build.resolvePath('plugins')), + mkdirp(build.resolvePath('data')), + mkdirp(build.resolvePath('logs')), + ]); }, }; diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index c7d9f6997cdf2..9c3f370ba7e98 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -113,6 +113,8 @@ export async function runFpm( '--exclude', `usr/share/kibana/data`, '--exclude', + `usr/share/kibana/logs`, + '--exclude', 'run/kibana/.gitempty', // flags specific to the package we are building, supplied by tasks below @@ -129,6 +131,9 @@ export async function runFpm( // copy the data directory at /var/lib/kibana `${resolveWithTrailingSlash(fromBuild('data'))}=/var/lib/kibana/`, + // copy the logs directory at /var/log/kibana + `${resolveWithTrailingSlash(fromBuild('logs'))}=/var/log/kibana/`, + // copy package configurations `${resolveWithTrailingSlash(__dirname, 'service_templates/systemd/')}=/`, diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index feadbbab5a4ca..9ebdcb5e4d05f 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -11,7 +11,7 @@ jest.mock('crypto', () => ({ })); jest.mock('@kbn/utils', () => ({ - getDataPath: () => '/mock/kibana/data/path', + getLogsPath: () => '/mock/kibana/logs/path', })); import { loggingSystemMock } from 'src/core/server/mocks'; @@ -1720,7 +1720,7 @@ describe('createConfig()', () => { ).audit.appender ).toMatchInlineSnapshot(` Object { - "fileName": "/mock/kibana/data/path/audit.log", + "fileName": "/mock/kibana/logs/path/audit.log", "layout": Object { "type": "json", }, diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index ba0d0d35d8ddd..f993707bd8d9e 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -12,7 +12,7 @@ import path from 'path'; import type { Type, TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { getDataPath } from '@kbn/utils'; +import { getLogsPath } from '@kbn/utils'; import type { AppenderConfigType, Logger } from 'src/core/server'; import { config as coreConfig } from '../../../../src/core/server'; @@ -378,7 +378,7 @@ export function createConfig( config.audit.appender ?? ({ type: 'rolling-file', - fileName: path.join(getDataPath(), 'audit.log'), + fileName: path.join(getLogsPath(), 'audit.log'), layout: { type: 'json', }, From 89f9c8f9fb79cfe4c8955c4732673625af97dcee Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 10 Nov 2021 11:00:45 +0100 Subject: [PATCH 77/98] [Exploatory view] Fix duplicate breakdowns (#117304) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../exploratory_view/series_editor/columns/series_actions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx index 37b5b1571f84d..c1462ce74b426 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -30,7 +30,7 @@ export function SeriesActions({ seriesId, series, seriesConfig, onEditClick }: P if (allSeries.find(({ name }) => name === copySeriesId)) { copySeriesId = copySeriesId + allSeries.length; } - setSeries(allSeries.length, { ...series, name: copySeriesId }); + setSeries(allSeries.length, { ...series, name: copySeriesId, breakdown: undefined }); }; const toggleSeries = () => { From 4ef23ae668133416bb3d9be446c1a494f7b55cb9 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 10 Nov 2021 11:04:41 +0100 Subject: [PATCH 78/98] Unskip QueryTopRow tests (#118014) --- .../ui/query_string_input/query_bar_top_row.test.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index 0541e12cf8172..453e74c9fad5b 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -10,10 +10,9 @@ import { mockPersistedLogFactory } from './query_string_input.test.mocks'; import React from 'react'; import { mount } from 'enzyme'; -import { waitFor } from '@testing-library/dom'; import { render } from '@testing-library/react'; -import { QueryBarTopRow } from './'; +import QueryBarTopRow from './query_bar_top_row'; import { coreMock } from '../../../../../core/public/mocks'; import { dataPluginMock } from '../../mocks'; @@ -103,8 +102,7 @@ function wrapQueryBarTopRowInContext(testProps: any) { ); } -// Failing: See https://github.com/elastic/kibana/issues/92528 -describe.skip('QueryBarTopRowTopRow', () => { +describe('QueryBarTopRowTopRow', () => { const QUERY_INPUT_SELECTOR = 'QueryStringInputUI'; const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; const TIMEPICKER_DURATION = '[data-shared-timefilter-duration]'; @@ -113,7 +111,7 @@ describe.skip('QueryBarTopRowTopRow', () => { jest.clearAllMocks(); }); - it('Should render query and time picker', async () => { + it('Should render query and time picker', () => { const { getByText, getByTestId } = render( wrapQueryBarTopRowInContext({ query: kqlQuery, @@ -124,8 +122,8 @@ describe.skip('QueryBarTopRowTopRow', () => { }) ); - await waitFor(() => getByText(kqlQuery.query)); - await waitFor(() => getByTestId('superDatePickerShowDatesButton')); + expect(getByText(kqlQuery.query)).toBeInTheDocument(); + expect(getByTestId('superDatePickerShowDatesButton')).toBeInTheDocument(); }); it('Should create a unique PersistedLog based on the appName and query language', () => { From 932d797b9cd318f8702b33faafb49f9c1e7c2d1c Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 10 Nov 2021 11:13:22 +0100 Subject: [PATCH 79/98] [Discover] Remove kibanaLegacy dependency (#117981) --- src/plugins/discover/kibana.json | 1 - src/plugins/discover/public/build_services.ts | 3 --- src/plugins/discover/public/plugin.tsx | 3 --- src/plugins/discover/tsconfig.json | 1 - 4 files changed, 8 deletions(-) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 791ce54a0cb1b..92871ca6d5e17 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -9,7 +9,6 @@ "embeddable", "inspector", "fieldFormats", - "kibanaLegacy", "urlForwarding", "navigation", "uiActions", diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index a6b175e34bd13..6003411e647c5 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -32,7 +32,6 @@ import { Storage } from '../../kibana_utils/public'; import { DiscoverStartPlugins } from './plugin'; import { getHistory } from './kibana_services'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; @@ -58,7 +57,6 @@ export interface DiscoverServices { metadata: { branch: string }; navigation: NavigationPublicPluginStart; share?: SharePluginStart; - kibanaLegacy: KibanaLegacyStart; urlForwarding: UrlForwardingStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; @@ -97,7 +95,6 @@ export function buildServices( }, navigation: plugins.navigation, share: plugins.share, - kibanaLegacy: plugins.kibanaLegacy, urlForwarding: plugins.urlForwarding, timefilter: plugins.data.query.timefilter.timefilter, toastNotifications: core.notifications.toasts, diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 766b2827c7cbd..ec95a82a5088e 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -23,7 +23,6 @@ import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public' import { ChartsPluginStart } from 'src/plugins/charts/public'; import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public'; -import { KibanaLegacySetup, KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; @@ -165,7 +164,6 @@ export interface DiscoverSetupPlugins { share?: SharePluginSetup; uiActions: UiActionsSetup; embeddable: EmbeddableSetup; - kibanaLegacy: KibanaLegacySetup; urlForwarding: UrlForwardingSetup; home?: HomePublicPluginSetup; data: DataPublicPluginSetup; @@ -182,7 +180,6 @@ export interface DiscoverStartPlugins { data: DataPublicPluginStart; fieldFormats: FieldFormatsStart; share?: SharePluginStart; - kibanaLegacy: KibanaLegacyStart; urlForwarding: UrlForwardingStart; inspector: InspectorPublicPluginStart; savedObjects: SavedObjectsStart; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index eb739e673cacd..86534268c578a 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -22,7 +22,6 @@ { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, - { "path": "../kibana_legacy/tsconfig.json" }, { "path": "../index_pattern_field_editor/tsconfig.json"}, { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, From 9a8499cd4ac625a4ec4d33a8b08f7d7cf15fe463 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 10 Nov 2021 11:25:30 +0100 Subject: [PATCH 80/98] [Security Solutions] Fix AsyncResource typescript errors (#116439) --- .../endpoint_hosts/store/middleware.test.ts | 5 +- .../pages/endpoint_hosts/store/middleware.ts | 25 +-- .../pages/event_filters/store/middleware.ts | 12 +- .../event_filters/store/selectors.test.ts | 11 +- .../store/middleware.ts | 41 ++-- .../policy_trusted_apps_middleware.ts | 40 ++-- .../list/policy_trusted_apps_list.test.tsx | 5 +- .../pages/trusted_apps/store/middleware.ts | 4 - .../public/management/state/README.md | 201 ++++++++++++++++++ .../state/async_resource_builders.ts | 31 ++- .../management/state/async_resource_state.ts | 4 +- 11 files changed, 297 insertions(+), 82 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/state/README.md diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index ba08d0d2d0dcd..8405320198615 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -30,7 +30,6 @@ import { endpointMiddlewareFactory } from './middleware'; import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/routing'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; import { - createUninitialisedResourceState, createLoadingResourceState, FailedResourceState, isFailedResourceState, @@ -255,9 +254,7 @@ describe('endpoint list middleware', () => { const dispatchGetActivityLogLoading = () => { dispatch({ type: 'endpointDetailsActivityLogChanged', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2345 - payload: createLoadingResourceState({ previousState: createUninitialisedResourceState() }), + payload: createLoadingResourceState(), }); }; 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 ec9672b645ca3..9f8c280fac30b 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 @@ -59,7 +59,7 @@ import { sendGetAgentPolicyList, sendGetFleetAgentsWithEndpoint, } from '../../policy/store/services/ingest'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE, PackageListItem } from '../../../../../../fleet/common'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../fleet/common'; import { ENDPOINT_ACTION_LOG_ROUTE, HOST_METADATA_GET_ROUTE, @@ -69,6 +69,7 @@ import { METADATA_UNITED_INDEX, } from '../../../../../common/endpoint/constants'; import { + asStaleResourceState, createFailedResourceState, createLoadedResourceState, createLoadingResourceState, @@ -284,9 +285,9 @@ const handleIsolateEndpointHost = async ( dispatch({ type: 'endpointIsolationRequestStateChange', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2345 - payload: createLoadingResourceState(getCurrentIsolationRequestState(state)), + payload: createLoadingResourceState( + asStaleResourceState(getCurrentIsolationRequestState(state)) + ), }); try { @@ -320,9 +321,7 @@ async function getEndpointPackageInfo( dispatch({ type: 'endpointPackageInfoStateChanged', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2345 - payload: createLoadingResourceState(endpointPackageInfo(state)), + payload: createLoadingResourceState(asStaleResourceState(endpointPackageInfo(state))), }); try { @@ -651,9 +650,7 @@ async function endpointDetailsActivityLogChangedMiddleware({ const { getState, dispatch } = store; dispatch({ type: 'endpointDetailsActivityLogChanged', - // ts error to be fixed when AsyncResourceState is refactored (#830) - // @ts-expect-error - payload: createLoadingResourceState(getActivityLogData(getState())), + payload: createLoadingResourceState(asStaleResourceState(getActivityLogData(getState()))), }); try { @@ -708,9 +705,7 @@ async function endpointDetailsActivityLogPagingMiddleware({ }); dispatch({ type: 'endpointDetailsActivityLogChanged', - // ts error to be fixed when AsyncResourceState is refactored (#830) - // @ts-expect-error - payload: createLoadingResourceState(getActivityLogData(getState())), + payload: createLoadingResourceState(asStaleResourceState(getActivityLogData(getState()))), }); const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { agent_id: selectedAgent(getState()), @@ -781,9 +776,7 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E dispatch({ type: 'metadataTransformStatsChanged', - // ts error to be fixed when AsyncResourceState is refactored (#830) - // @ts-expect-error - payload: createLoadingResourceState(getMetadataTransformStats(state)), + payload: createLoadingResourceState(asStaleResourceState(getMetadataTransformStats(state))), }); try { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts index c77494aad2de2..bc59b4d2c3bb3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts @@ -44,6 +44,7 @@ import { EventFiltersServiceGetListOptions, } from '../types'; import { + asStaleResourceState, createFailedResourceState, createLoadedResourceState, createLoadingResourceState, @@ -203,8 +204,9 @@ const checkIfEventFilterDataExist: MiddlewareActionHandler = async ( ) => { dispatch({ type: 'eventFiltersListPageDataExistsChanged', - // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) - payload: createLoadingResourceState(getListPageDataExistsState(getState())), + payload: createLoadingResourceState( + asStaleResourceState(getListPageDataExistsState(getState())) + ), }); try { @@ -231,9 +233,8 @@ const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFilt dispatch({ type: 'eventFiltersListPageDataChanged', payload: { - // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) type: 'LoadingResourceState', - previousState: getCurrentListPageDataState(state), + previousState: asStaleResourceState(getCurrentListPageDataState(state)), }, }); @@ -298,8 +299,7 @@ const eventFilterDeleteEntry: MiddlewareActionHandler = async ( dispatch({ type: 'eventFilterDeleteStatusChanged', - // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) - payload: createLoadingResourceState(getDeletionState(state).status), + payload: createLoadingResourceState(asStaleResourceState(getDeletionState(state).status)), }); try { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts index 7947f5b011ff6..631f23c879169 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts @@ -31,6 +31,7 @@ import { EventFiltersListPageState, EventFiltersPageLocation } from '../types'; import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { + asStaleResourceState, createFailedResourceState, createLoadedResourceState, createLoadingResourceState, @@ -60,9 +61,7 @@ describe('event filters selectors', () => { ) => { previousStateWhileLoading = previousState; - // will be fixed when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2345 - initialState.listPage.data = createLoadingResourceState(previousState); + initialState.listPage.data = createLoadingResourceState(asStaleResourceState(previousState)); }; beforeEach(() => { @@ -204,9 +203,9 @@ describe('event filters selectors', () => { expect(getListPageDoesDataExist(initialState)).toBe(false); // Set DataExists to Loading - // will be fixed when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2345 - initialState.listPage.dataExist = createLoadingResourceState(initialState.listPage.dataExist); + initialState.listPage.dataExist = createLoadingResourceState( + asStaleResourceState(initialState.listPage.dataExist) + ); expect(getListPageDoesDataExist(initialState)).toBe(false); // Set DataExists to Failure diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts index ad99e86abb231..3eca607f0c747 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts @@ -23,6 +23,7 @@ import { createFailedResourceState, createLoadedResourceState, createLoadingResourceState, + asStaleResourceState, } from '../../../state/async_resource_builders'; import { deleteHostIsolationExceptionItems, @@ -33,6 +34,7 @@ import { } from '../service'; import { HostIsolationExceptionsPageState } from '../types'; import { getCurrentListPageDataState, getCurrentLocation, getItemToDelete } from './selector'; +import { HostIsolationExceptionsPageAction } from './action'; export const SEARCHABLE_FIELDS: Readonly = [`name`, `description`, `entries.value`]; @@ -69,19 +71,21 @@ export const createHostIsolationExceptionsPageMiddleware = ( }; async function createHostIsolationException( - store: ImmutableMiddlewareAPI, + store: ImmutableMiddlewareAPI< + HostIsolationExceptionsPageState, + HostIsolationExceptionsPageAction + >, http: HttpStart ) { const { dispatch } = store; const entry = transformNewItemOutput( store.getState().form.entry as CreateExceptionListItemSchema ); + dispatch({ type: 'hostIsolationExceptionsFormStateChanged', payload: { - // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) type: 'LoadingResourceState', - previousState: entry, }, }); try { @@ -102,7 +106,10 @@ async function createHostIsolationException( } async function loadHostIsolationExceptionsList( - store: ImmutableMiddlewareAPI, + store: ImmutableMiddlewareAPI< + HostIsolationExceptionsPageState, + HostIsolationExceptionsPageAction + >, http: HttpStart ) { const { dispatch } = store; @@ -121,11 +128,9 @@ async function loadHostIsolationExceptionsList( dispatch({ type: 'hostIsolationExceptionsPageDataChanged', - payload: { - // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) - type: 'LoadingResourceState', - previousState: getCurrentListPageDataState(store.getState()), - }, + payload: createLoadingResourceState( + asStaleResourceState(getCurrentListPageDataState(store.getState())) + ), }); const entries = await getHostIsolationExceptionItems(query); @@ -152,7 +157,10 @@ function isHostIsolationExceptionsPage(location: Immutable) { } async function deleteHostIsolationExceptionsItem( - store: ImmutableMiddlewareAPI, + store: ImmutableMiddlewareAPI< + HostIsolationExceptionsPageState, + HostIsolationExceptionsPageAction + >, http: HttpSetup ) { const { dispatch } = store; @@ -160,13 +168,12 @@ async function deleteHostIsolationExceptionsItem( if (itemToDelete === undefined) { return; } + try { dispatch({ type: 'hostIsolationExceptionsDeleteStatusChanged', payload: { - // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) type: 'LoadingResourceState', - previousState: store.getState().deletion.status, }, }); @@ -186,7 +193,10 @@ async function deleteHostIsolationExceptionsItem( } async function loadHostIsolationExceptionsItem( - store: ImmutableMiddlewareAPI, + store: ImmutableMiddlewareAPI< + HostIsolationExceptionsPageState, + HostIsolationExceptionsPageAction + >, http: HttpSetup, id: string ) { @@ -208,7 +218,10 @@ async function loadHostIsolationExceptionsItem( } } async function updateHostIsolationExceptionsItem( - store: ImmutableMiddlewareAPI, + store: ImmutableMiddlewareAPI< + HostIsolationExceptionsPageState, + HostIsolationExceptionsPageAction + >, http: HttpSetup, exception: ImmutableObject ) { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts index 782c659b3d765..8bb13d6fcd3b8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts @@ -31,6 +31,7 @@ import { getDoesAnyTrustedAppExistsIsLoading, } from '../selectors'; import { + GetTrustedAppsListResponse, Immutable, MaybeImmutable, PutTrustedAppUpdateResponse, @@ -39,10 +40,10 @@ import { import { ImmutableMiddlewareAPI } from '../../../../../../common/store'; import { TrustedAppsService } from '../../../../trusted_apps/service'; import { + asStaleResourceState, createFailedResourceState, createLoadedResourceState, createLoadingResourceState, - createUninitialisedResourceState, isLoadingResourceState, isUninitialisedResourceState, } from '../../../../../state'; @@ -113,9 +114,7 @@ const checkIfThereAreAssignableTrustedApps = async ( store.dispatch({ type: 'policyArtifactsAssignableListExistDataChanged', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2345 - payload: createLoadingResourceState({ previousState: createUninitialisedResourceState() }), + payload: createLoadingResourceState(), }); try { const trustedApps = await trustedAppsService.getTrustedAppsList({ @@ -131,9 +130,7 @@ const checkIfThereAreAssignableTrustedApps = async ( } catch (err) { store.dispatch({ type: 'policyArtifactsAssignableListExistDataChanged', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2741 - payload: createFailedResourceState(err.body ?? err), + payload: createFailedResourceState(err.body ?? err), }); } }; @@ -148,9 +145,7 @@ const checkIfAnyTrustedApp = async ( } store.dispatch({ type: 'policyArtifactsDeosAnyTrustedAppExists', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-ignore - payload: createLoadingResourceState({ previousState: createUninitialisedResourceState() }), + payload: createLoadingResourceState(), }); try { const trustedApps = await trustedAppsService.getTrustedAppsList({ @@ -180,9 +175,7 @@ const searchTrustedApps = async ( store.dispatch({ type: 'policyArtifactsAssignableListPageDataChanged', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2345 - payload: createLoadingResourceState({ previousState: createUninitialisedResourceState() }), + payload: createLoadingResourceState(), }); try { @@ -212,9 +205,7 @@ const searchTrustedApps = async ( } catch (err) { store.dispatch({ type: 'policyArtifactsAssignableListPageDataChanged', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-expect-error TS2322 - payload: createFailedResourceState(err.body ?? err), + payload: createFailedResourceState(err.body ?? err), }); } }; @@ -229,9 +220,7 @@ const updateTrustedApps = async ( store.dispatch({ type: 'policyArtifactsUpdateTrustedAppsChanged', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-expect-error - payload: createLoadingResourceState({ previousState: createUninitialisedResourceState() }), + payload: createLoadingResourceState(), }); try { @@ -268,8 +257,9 @@ const fetchPolicyTrustedAppsIfNeeded = async ( if (forceFetch || doesPolicyTrustedAppsListNeedUpdate(state)) { dispatch({ type: 'assignedTrustedAppsListStateChanged', - // @ts-ignore will be fixed when AsyncResourceState is refactored (#830) - payload: createLoadingResourceState(getCurrentPolicyAssignedTrustedAppsState(state)), + payload: createLoadingResourceState( + asStaleResourceState(getCurrentPolicyAssignedTrustedAppsState(state)) + ), }); try { @@ -320,8 +310,7 @@ const fetchAllPoliciesIfNeeded = async ( dispatch({ type: 'policyDetailsListOfAllPoliciesStateChanged', - // @ts-ignore will be fixed when AsyncResourceState is refactored (#830) - payload: createLoadingResourceState(currentPoliciesState), + payload: createLoadingResourceState(asStaleResourceState(currentPoliciesState)), }); try { @@ -357,8 +346,9 @@ const removeTrustedAppsFromPolicy = async ( dispatch({ type: 'policyDetailsTrustedAppsRemoveListStateChanged', - // @ts-expect-error will be fixed when AsyncResourceState is refactored (#830) - payload: createLoadingResourceState(getCurrentTrustedAppsRemoveListState(state)), + payload: createLoadingResourceState( + asStaleResourceState(getCurrentTrustedAppsRemoveListState(state)) + ), }); try { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index b136eef9566eb..0f26947c06071 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -15,7 +15,6 @@ import React from 'react'; import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils'; import { createLoadingResourceState, - createUninitialisedResourceState, isFailedResourceState, isLoadedResourceState, } from '../../../../../state'; @@ -118,9 +117,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { act(() => { appTestContext.store.dispatch({ type: 'policyArtifactsDeosAnyTrustedAppExists', - // Ignore will be fixed with when AsyncResourceState is refactored (#830) - // @ts-ignore - payload: createLoadingResourceState({ previousState: createUninitialisedResourceState() }), + payload: createLoadingResourceState(), }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 0de5761ccf074..55199d703d1ca 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -63,7 +63,6 @@ import { editItemId, editingTrustedApp, getListItems, - editItemState, getCurrentLocationIncludedPolicies, getCurrentLocationExcludedPolicies, } from './selectors'; @@ -413,10 +412,7 @@ const fetchEditTrustedAppIfNeeded = async ( dispatch({ type: 'trustedAppCreationEditItemStateChanged', payload: { - // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) type: 'LoadingResourceState', - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - previousState: editItemState(currentState)!, }, }); diff --git a/x-pack/plugins/security_solution/public/management/state/README.md b/x-pack/plugins/security_solution/public/management/state/README.md new file mode 100644 index 0000000000000..e47c15b73d098 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/state/README.md @@ -0,0 +1,201 @@ +# AsyncResourceState + +>Note: This documentation is far from complete, please update it as you see fit. + +`AsyncResourceState` is a helper type that is used to keep track of resources that will be loaded via an API call and used to show data. It is an union of all these types: + +* `LoadingResourceState` +* `LoadedResourceState` +* `FailedResourceState` +* `UninitialisedResourceState` +* `StaleResourceState` (not part of AsyncResourceState, see next) + +`StaleResourceState` exists to represent all non-loading states. It is a union of `LoadedResourceState`, `FailedResourceState` and `UninitialisedResourceState` but does not include `LoadingResourceState` + +## Use case + +When you want to load a resource from an API you want to keep the status of said resource updated to render your view accordingly. `AsyncResourceState` works by wrapping the real `data` into the previously mentioned states and providing other helper functions to see if the data is available. + +e.g.: Show a list of elements coming from `/api/items`: + +*With helpers and builders* + +```typescript +const ListComponent = () => { + // the initial value of list is an UninitialisedResourceState. + const [list, setList] = useState(createUninitialisedResourceState()) + + useEffect( () => { + // set the list as loading, `createLoadingResourceState` can be used for this as well + setList(createLoadingResourceState(asStaleResourceState(list))) // see NOTE 1 + + try { + const data = await fetch("/api/items"); + // sets the data as loaded + setList(createLoadedResourceState(data)); + } catch(e) { + //set the error + setList(createFailedResourceState(e)); + } + + }, []) + + if (isFailedResourceState(list)) { + return ( ) + } + + return ( isLoadingResourceState(list) ? : { + // the initial value of list is an UninitialisedResourceState. You can also use the `createUninitialisedResourceState` helper for this + const [list, setList] = useState({ type: 'UninitialisedResourceState'}) + + useEffect( () => { + // set the list as loading, `createLoadingResourceState` can be used for this as well + setList({type: 'LoadingResourceState', previousState: list}); + + try { + const data = await fetch("/api/items"); + // sets the data as loaded + setList({type: 'LoadedResourceState', data: await data.json() }) + } catch(e) { + //set the error + setList({type: 'FailedResourceState', error: e, lastLoadedState: list }) + } + + }, []) + + if (list.type === 'FailedResourceState') { + return ( ) + } + + return ( list.type ==='LoadingResourceState' ? : *NOTE 1*: `createLoadingResourceState` can only accept a StaleResourceState. In this example `list` could be a `LoadingResourceState` if this code executes twice for example. To prevent type errors an `asStaleResourceState` is used that make sure to convert the current object into an acceptable one. + +## A redux use case + +The previous example is not too realistic because using react hooks there are easier ways to know if a resource is available or not and there are libraries that could handle this state for us. + +A more suited case for `AsyncResourceState` is using redux and actions when you want to keep all your state in a single place but you need a dedicated type to keep your resource loading/loaded state. This requires more boilerplate code to setup and it looks like this: + + +*State type definition* +```typescript +interface MyListPageState = { + entries: AsyncResourceState +} +``` + +*Actions definition* +```typescript +export MyListPageEntriesChanged = { + payload: MyListPageState['entries'] +} +``` + +*Reducer definition* +```typescript +export reducer = (state, action) => { + if (action.type === 'myListPageEntriesChanged'){ + return { + ...state, + entries: action.payload + } + } +} +``` + +*middleware definition (actual data load)* +```typescript +export const myListPageMiddleware = () => { + return (store) => (next) => async (action) => { + next(action); + + if (action.type === 'userChangedUrl' && isMyListPage(action.payload)) { + // set the loading state + dispatch({ + type: 'myListPageEntriesChanged', + // IMPORTANT: note the usage of the `asStaleResourceState` helper. Otherwise this will + // create types error because the current `entries` could be another LoadingResourceState + payload: createLoadingResourceState(asStaleResourceState(store.getState().entries)); + }) + try { + const data = await fetch("/api/items"); + + // set data loaded + dispatch({ + type: 'myListPageEntriesChanged', + payload: createLoadedResourceState(data) + }) + } catch(e) { + dispatch({ + type: 'myListPageEntriesChanged', + payload: createFailedResourceState(e) + }) + } + }) +``` + +*react component code* +```typescript +const ListComponent = () => { + const list = myListPageSelector( (state) => state.entries ); + + if (isFailedResourceState(list)) { + return ( ) + } + + return ( isLoadingResourceState(list) ? : (e); // Note +``` + +## The ImmutableObject problem + +If you are using redux, all the data coming from selectors is wrapped around an `ImmutableObject` data type. The `AsyncResourceState` is prepared to deal with this scenario, provided you use the helpers and builders available. + + diff --git a/x-pack/plugins/security_solution/public/management/state/async_resource_builders.ts b/x-pack/plugins/security_solution/public/management/state/async_resource_builders.ts index 0c18e121d25b1..f23d3397be6ad 100644 --- a/x-pack/plugins/security_solution/public/management/state/async_resource_builders.ts +++ b/x-pack/plugins/security_solution/public/management/state/async_resource_builders.ts @@ -13,13 +13,14 @@ import { UninitialisedResourceState, } from './async_resource_state'; import { ServerApiError } from '../../common/types'; +import { Immutable } from '../../../common/endpoint/types'; export const createUninitialisedResourceState = (): UninitialisedResourceState => { return { type: 'UninitialisedResourceState' }; }; export const createLoadingResourceState = ( - previousState: StaleResourceState + previousState?: StaleResourceState ): LoadingResourceState => { return { type: 'LoadingResourceState', @@ -44,3 +45,31 @@ export const createFailedResourceState = ( lastLoadedState, }; }; + +type MaybeStaleResourceState = + | LoadedResourceState + | FailedResourceState + | UninitialisedResourceState + | LoadingResourceState + | Immutable> + | Immutable> + | Immutable + | Immutable>; + +/** + * Takes an existing AsyncResourceState and transforms it into a StaleResourceState (not loading) + * Note: If a loading state is passed, the resource is returned as UninitialisedResourceState + */ +export const asStaleResourceState = ( + resource: MaybeStaleResourceState +): StaleResourceState => { + switch (resource.type) { + case 'LoadedResourceState': + return resource as LoadedResourceState; + case 'FailedResourceState': + return resource as FailedResourceState; + case 'UninitialisedResourceState': + case 'LoadingResourceState': + return createUninitialisedResourceState(); + } +}; diff --git a/x-pack/plugins/security_solution/public/management/state/async_resource_state.ts b/x-pack/plugins/security_solution/public/management/state/async_resource_state.ts index 1b6dec54ec0b3..ec8922da46191 100644 --- a/x-pack/plugins/security_solution/public/management/state/async_resource_state.ts +++ b/x-pack/plugins/security_solution/public/management/state/async_resource_state.ts @@ -38,7 +38,7 @@ export interface UninitialisedResourceState { */ export interface LoadingResourceState { type: 'LoadingResourceState'; - previousState: StaleResourceState; + previousState?: StaleResourceState; } /** @@ -121,7 +121,7 @@ export const getLastLoadedResourceState = ( ): Immutable> | undefined => { if (isLoadedResourceState(state)) { return state; - } else if (isLoadingResourceState(state)) { + } else if (isLoadingResourceState(state) && state.previousState !== undefined) { return getLastLoadedResourceState(state.previousState); } else if (isFailedResourceState(state)) { return state.lastLoadedState; From b7974a331eb0e3daabecd63c60a9c28f7cb005a2 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Wed, 10 Nov 2021 05:40:18 -0500 Subject: [PATCH 81/98] [Observability] [Exploratory View] prevent chart from rerendering on report type changes (#118085) --- .../hooks/use_lens_attributes.test.tsx | 73 +++++++++++++++++++ .../hooks/use_lens_attributes.ts | 6 +- .../hooks/use_series_storage.test.tsx | 61 +++++++++++++++- .../hooks/use_series_storage.tsx | 13 ++-- 4 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx new file mode 100644 index 0000000000000..3334b69e5becc --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { allSeriesKey, reportTypeKey, UrlStorageContextProvider } from './use_series_storage'; +import { renderHook } from '@testing-library/react-hooks'; +import { useLensAttributes } from './use_lens_attributes'; +import { ReportTypes } from '../configurations/constants'; +import { mockIndexPattern } from '../rtl_helpers'; +import { createKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { TRANSACTION_DURATION } from '../configurations/constants/elasticsearch_fieldnames'; +import * as lensAttributes from '../configurations/lens_attributes'; +import * as indexPattern from './use_app_index_pattern'; +import * as theme from '../../../../hooks/use_theme'; + +const mockSingleSeries = [ + { + name: 'performance-distribution', + dataType: 'ux', + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + selectedMetricField: TRANSACTION_DURATION, + reportDefinitions: { 'service.name': ['elastic-co'] }, + }, +]; + +describe('useExpViewTimeRange', function () { + const storage = createKbnUrlStateStorage({ useHash: false }); + // @ts-ignore + jest.spyOn(indexPattern, 'useAppIndexPatternContext').mockReturnValue({ + indexPatterns: { + ux: mockIndexPattern, + apm: mockIndexPattern, + mobile: mockIndexPattern, + infra_logs: mockIndexPattern, + infra_metrics: mockIndexPattern, + synthetics: mockIndexPattern, + }, + }); + jest.spyOn(theme, 'useTheme').mockReturnValue({ + // @ts-ignore + eui: { + euiColorVis1: '#111111', + }, + }); + const lensAttributesSpy = jest.spyOn(lensAttributes, 'LensAttributes'); + + function Wrapper({ children }: { children: JSX.Element }) { + return {children}; + } + + it('updates lens attributes with report type from storage', async function () { + await storage.set(allSeriesKey, mockSingleSeries); + await storage.set(reportTypeKey, ReportTypes.KPI); + + renderHook(() => useLensAttributes(), { + wrapper: Wrapper, + }); + + expect(lensAttributesSpy).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + seriesConfig: expect.objectContaining({ reportType: ReportTypes.KPI }), + }), + ]) + ); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index ae3d57b3c9652..f81494e8f9ac7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -13,6 +13,7 @@ import { AllSeries, allSeriesKey, convertAllShortSeries, + reportTypeKey, useSeriesStorage, } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; @@ -93,11 +94,12 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null return useMemo(() => { // we only use the data from url to apply, since that gets updated to apply changes const allSeriesT: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []); + const reportTypeT: ReportViewType = storage.get(reportTypeKey) as ReportViewType; - if (isEmpty(indexPatterns) || isEmpty(allSeriesT) || !reportType) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesT) || !reportTypeT) { return null; } - const layerConfigs = getLayerConfigs(allSeriesT, reportType, theme, indexPatterns); + const layerConfigs = getLayerConfigs(allSeriesT, reportTypeT, theme, indexPatterns); if (layerConfigs.length < 1) { return null; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index 1d23796b5bf55..6abb0416d0908 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -9,9 +9,10 @@ import React, { useEffect } from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; import { Route, Router } from 'react-router-dom'; import { render } from '@testing-library/react'; -import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; +import { UrlStorageContextProvider, useSeriesStorage, reportTypeKey } from './use_series_storage'; import { getHistoryFromUrl } from '../rtl_helpers'; import type { AppDataType } from '../types'; +import { ReportTypes } from '../configurations/constants'; import * as useTrackMetric from '../../../../hooks/use_track_metric'; const mockSingleSeries = [ @@ -163,6 +164,64 @@ describe('userSeriesStorage', function () { ]); }); + it('sets reportType when calling applyChanges', () => { + const setStorage = jest.fn(); + function wrapper({ children }: { children: React.ReactElement }) { + return ( + + key === 'sr' ? mockMultipleSeries : 'kpi-over-time' + ), + set: setStorage, + }} + > + {children} + + ); + } + const { result } = renderHook(() => useSeriesStorage(), { wrapper }); + + act(() => { + result.current.setReportType(ReportTypes.DISTRIBUTION); + }); + + act(() => { + result.current.applyChanges(); + }); + + expect(setStorage).toBeCalledWith(reportTypeKey, ReportTypes.DISTRIBUTION); + }); + + it('returns reportType in state, not url storage, from hook', () => { + const setStorage = jest.fn(); + function wrapper({ children }: { children: React.ReactElement }) { + return ( + + key === 'sr' ? mockMultipleSeries : 'kpi-over-time' + ), + set: setStorage, + }} + > + {children} + + ); + } + const { result } = renderHook(() => useSeriesStorage(), { wrapper }); + + act(() => { + result.current.setReportType(ReportTypes.DISTRIBUTION); + }); + + expect(result.current.reportType).toEqual(ReportTypes.DISTRIBUTION); + }); + it('ensures that telemetry is called', () => { const trackEvent = jest.fn(); jest.spyOn(useTrackMetric, 'useUiTracker').mockReturnValue(trackEvent); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index 2e8369bd1ddd4..3fca13f7978d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -32,7 +32,7 @@ export interface SeriesContextValue { setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; getSeries: (seriesIndex: number) => SeriesUrl | undefined; removeSeries: (seriesIndex: number) => void; - setReportType: (reportType: string) => void; + setReportType: (reportType: ReportViewType) => void; storage: IKbnUrlStateStorage | ISessionStorageStateStorage; reportType: ReportViewType; } @@ -59,8 +59,8 @@ export function UrlStorageContextProvider({ const [lastRefresh, setLastRefresh] = useState(() => Date.now()); - const [reportType, setReportType] = useState( - () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' + const [reportType, setReportType] = useState( + () => ((storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '') as ReportViewType ); const [firstSeries, setFirstSeries] = useState(); @@ -97,10 +97,6 @@ export function UrlStorageContextProvider({ }); }, []); - useEffect(() => { - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - }, [reportType, storage]); - const removeSeries = useCallback((seriesIndex: number) => { setAllSeries((prevAllSeries) => prevAllSeries.filter((seriesT, index) => index !== seriesIndex) @@ -117,6 +113,7 @@ export function UrlStorageContextProvider({ const applyChanges = useCallback( (onApply?: () => void) => { const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); setLastRefresh(Date.now()); @@ -140,7 +137,7 @@ export function UrlStorageContextProvider({ lastRefresh, setLastRefresh, setReportType, - reportType: storage.get(reportTypeKey) as ReportViewType, + reportType, firstSeries: firstSeries!, }; return {children}; From 7ab3593fb73506e29e9608b4c482f9aa22152039 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 10 Nov 2021 11:43:48 +0100 Subject: [PATCH 82/98] Fix FSREQCALLBACK open handles in Jest tests (#117984) * upgrade mock-fs * elastic@ email address --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 587c3a31316fd..39bfc891e65b3 100644 --- a/package.json +++ b/package.json @@ -761,7 +761,7 @@ "mocha-junit-reporter": "^2.0.0", "mochawesome": "^6.2.1", "mochawesome-merge": "^4.2.0", - "mock-fs": "^5.1.1", + "mock-fs": "^5.1.2", "mock-http-server": "1.3.0", "ms-chromium-edge-driver": "^0.4.2", "multimatch": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 40b4d29f75ba3..7c2f64a0983b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20624,10 +20624,10 @@ mochawesome@^6.2.1: strip-ansi "^6.0.0" uuid "^7.0.3" -mock-fs@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.1.tgz#d4c95e916abf400664197079d7e399d133bb6048" - integrity sha512-p/8oZ3qvfKGPw+4wdVCyjDxa6wn2tP0TCf3WXC1UyUBAevezPn1TtOoxtMYVbZu/S/iExg+Ghed1busItj2CEw== +mock-fs@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" + integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== mock-http-server@1.3.0: version "1.3.0" From f7163878c0f291ab11d2b3daa7ec9df19786198d Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 10 Nov 2021 13:41:47 +0100 Subject: [PATCH 83/98] [SO migration] remove v1 implementation (#118000) * remove v1 implementation * fix type * remove unused mock * expose kibanaVersion again * fix migrator mock * move KibanaMigrator out of the kibana subfolder * fix imports * moves migrationsv2 into migrations * fix test mocking --- .../server/saved_objects/migrations/README.md | 708 +++++++++++----- .../kibana_migrator.test.ts.snap | 0 ...grations_state_action_machine.test.ts.snap | 0 ...lk_overwrite_transformed_documents.test.ts | 0 .../bulk_overwrite_transformed_documents.ts | 0 .../actions/calculate_exclude_filters.test.ts | 0 .../actions/calculate_exclude_filters.ts | 0 .../catch_retryable_es_client_errors.test.ts | 0 .../catch_retryable_es_client_errors.ts | 0 .../actions/check_for_unknown_docs.test.ts | 0 .../actions/check_for_unknown_docs.ts | 0 .../actions/clone_index.test.ts | 0 .../actions/clone_index.ts | 0 .../actions/close_pit.test.ts | 0 .../actions/close_pit.ts | 0 .../actions/constants.ts | 0 .../actions/create_index.test.ts | 0 .../actions/create_index.ts | 0 .../actions/es_errors.test.ts | 0 .../actions/es_errors.ts | 0 .../actions/fetch_indices.test.ts | 0 .../actions/fetch_indices.ts | 0 .../actions/index.ts | 0 .../actions/integration_tests/actions.test.ts | 2 +- .../archives/7.7.2_xpack_100k_obj.zip | Bin .../integration_tests/es_errors.test.ts | 0 .../actions/open_pit.test.ts | 0 .../actions/open_pit.ts | 0 .../actions/pickup_updated_mappings.test.ts | 0 .../actions/pickup_updated_mappings.ts | 0 .../actions/read_with_pit.test.ts | 0 .../actions/read_with_pit.ts | 0 .../actions/refresh_index.test.ts | 0 .../actions/refresh_index.ts | 0 .../actions/reindex.test.ts | 0 .../actions/reindex.ts | 0 .../actions/remove_write_block.test.ts | 0 .../actions/remove_write_block.ts | 0 .../search_for_outdated_documents.test.ts | 0 .../actions/search_for_outdated_documents.ts | 0 .../actions/set_write_block.test.ts | 0 .../actions/set_write_block.ts | 0 .../actions/transform_docs.ts | 0 .../actions/update_aliases.test.ts | 0 .../actions/update_aliases.ts | 0 .../update_and_pickup_mappings.test.ts | 0 .../actions/update_and_pickup_mappings.ts | 0 .../actions/verify_reindex.ts | 0 .../wait_for_index_status_yellow.test.ts | 0 .../actions/wait_for_index_status_yellow.ts | 0 ...t_for_pickup_updated_mappings_task.test.ts | 0 .../wait_for_pickup_updated_mappings_task.ts | 0 .../actions/wait_for_reindex_task.test.ts | 0 .../actions/wait_for_reindex_task.ts | 0 .../actions/wait_for_task.test.ts | 0 .../actions/wait_for_task.ts | 0 .../__snapshots__/elastic_index.test.ts.snap | 40 - .../migrations/core/call_cluster.ts | 28 - ...sable_unknown_type_mapping_fields.test.ts} | 2 +- .../disable_unknown_type_mapping_fields.ts | 60 ++ .../migrations/core/elastic_index.test.ts | 702 ---------------- .../migrations/core/elastic_index.ts | 425 ---------- .../saved_objects/migrations/core/index.ts | 8 +- .../migrations/core/index_migrator.test.ts | 478 ----------- .../migrations/core/index_migrator.ts | 194 ----- .../migrations/core/migration_context.ts | 188 ----- .../core/migration_coordinator.test.ts | 75 -- .../migrations/core/migration_coordinator.ts | 124 --- .../core/migration_es_client.test.mock.ts | 12 - .../core/migration_es_client.test.ts | 55 -- .../migrations/core/migration_es_client.ts | 78 -- .../kibana_migrator.ts => core/types.ts} | 20 +- .../migrations/core/unused_types.ts | 63 ++ .../server/saved_objects/migrations/index.ts | 4 +- .../initial_state.test.ts | 0 .../initial_state.ts | 2 +- .../integration_tests/.gitignore | 0 .../7.7.2_xpack_100k.test.ts | 0 .../7_13_0_failed_action_tasks.test.ts | 0 .../7_13_0_transform_failures.test.ts | 0 .../7_13_0_unknown_types.test.ts | 0 .../archives/7.13.0_5k_so_node_01.zip | Bin .../archives/7.13.0_5k_so_node_02.zip | Bin .../archives/7.13.0_concurrent_5k_foo.zip | Bin .../archives/7.13.0_with_corrupted_so.zip | Bin .../archives/7.13.0_with_unknown_so.zip | Bin .../7.13.2_so_with_multiple_namespaces.zip | Bin .../7.13_1.5k_failed_action_tasks.zip | Bin .../7.14.0_xpack_sample_saved_objects.zip | Bin .../7.3.0_xpack_sample_saved_objects.zip | Bin .../archives/7.7.2_xpack_100k_obj.zip | Bin ...13_corrupt_and_transform_failures_docs.zip | Bin .../8.0.0_document_migration_failure.zip | Bin ....0_migrated_with_corrupt_outdated_docs.zip | Bin .../8.0.0_migrated_with_outdated_docs.zip | Bin ...1_migrations_sample_data_saved_objects.zip | Bin .../batch_size_bytes.test.ts | 0 ...ze_bytes_exceeds_es_content_length.test.ts | 0 .../integration_tests/cleanup.test.ts | 0 .../collects_corrupt_docs.test.ts | 0 .../corrupt_outdated_docs.test.ts | 0 .../migration_from_older_v1.test.ts | 0 .../migration_from_same_v1.test.ts | 0 .../multiple_es_nodes.test.ts | 0 .../multiple_kibana_nodes.test.ts | 0 .../integration_tests/outdated_docs.test.ts | 0 .../integration_tests/rewriting_id.test.ts | 0 .../type_registrations.test.ts | 0 .../saved_objects/migrations/kibana/index.ts | 10 - .../{kibana => }/kibana_migrator.mock.ts | 14 +- .../{kibana => }/kibana_migrator.test.ts | 14 +- .../{kibana => }/kibana_migrator.ts | 30 +- .../migrations_state_action_machine.test.ts | 2 +- .../migrations_state_action_machine.ts | 2 +- .../migrations_state_machine_cleanup.mocks.ts | 0 .../migrations_state_machine_cleanup.ts | 2 +- .../model/create_batches.test.ts | 0 .../model/create_batches.ts | 0 .../model/extract_errors.test.ts | 0 .../model/extract_errors.ts | 2 +- .../model/helpers.ts | 2 +- .../model/index.ts | 0 .../model/model.test.ts | 2 +- .../model/model.ts | 7 +- .../model/progress.test.ts | 0 .../model/progress.ts | 0 .../model/retry_state.test.ts | 2 +- .../model/retry_state.ts | 2 +- .../model/types.ts | 0 .../{migrationsv2 => migrations}/next.test.ts | 2 +- .../{migrationsv2 => migrations}/next.ts | 4 +- .../run_resilient_migrator.ts} | 0 .../types.ts => migrations/state.ts} | 22 +- .../state_action_machine.test.ts | 0 .../state_action_machine.ts | 0 .../test_helpers/retry.test.ts | 0 .../test_helpers/retry_async.ts | 0 .../server/saved_objects/migrations/types.ts | 27 +- .../saved_objects/migrationsv2/README.md | 504 ------------ .../integration_tests/migrate.test.mocks.ts | 4 +- .../saved_objects_service.test.mocks.ts | 4 +- .../saved_objects/saved_objects_service.ts | 15 +- .../service/lib/repository.test.ts | 2 +- .../lib/repository_create_repository.test.ts | 2 +- src/core/server/saved_objects/status.ts | 2 +- .../apis/saved_objects/index.ts | 1 - .../apis/saved_objects/migrations.ts | 763 ------------------ 147 files changed, 717 insertions(+), 3994 deletions(-) rename src/core/server/saved_objects/migrations/{kibana => }/__snapshots__/kibana_migrator.test.ts.snap (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/__snapshots__/migrations_state_action_machine.test.ts.snap (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/bulk_overwrite_transformed_documents.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/bulk_overwrite_transformed_documents.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/calculate_exclude_filters.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/calculate_exclude_filters.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/catch_retryable_es_client_errors.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/catch_retryable_es_client_errors.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/check_for_unknown_docs.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/check_for_unknown_docs.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/clone_index.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/clone_index.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/close_pit.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/close_pit.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/constants.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/create_index.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/create_index.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/es_errors.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/es_errors.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/fetch_indices.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/fetch_indices.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/index.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/integration_tests/actions.test.ts (99%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/integration_tests/es_errors.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/open_pit.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/open_pit.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/pickup_updated_mappings.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/pickup_updated_mappings.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/read_with_pit.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/read_with_pit.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/refresh_index.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/refresh_index.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/reindex.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/reindex.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/remove_write_block.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/remove_write_block.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/search_for_outdated_documents.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/search_for_outdated_documents.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/set_write_block.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/set_write_block.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/transform_docs.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/update_aliases.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/update_aliases.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/update_and_pickup_mappings.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/update_and_pickup_mappings.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/verify_reindex.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/wait_for_index_status_yellow.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/wait_for_index_status_yellow.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/wait_for_pickup_updated_mappings_task.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/wait_for_pickup_updated_mappings_task.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/wait_for_reindex_task.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/wait_for_reindex_task.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/wait_for_task.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/actions/wait_for_task.ts (100%) delete mode 100644 src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap delete mode 100644 src/core/server/saved_objects/migrations/core/call_cluster.ts rename src/core/server/saved_objects/migrations/core/{migration_context.test.ts => disable_unknown_type_mapping_fields.test.ts} (96%) create mode 100644 src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.ts delete mode 100644 src/core/server/saved_objects/migrations/core/elastic_index.test.ts delete mode 100644 src/core/server/saved_objects/migrations/core/elastic_index.ts delete mode 100644 src/core/server/saved_objects/migrations/core/index_migrator.test.ts delete mode 100644 src/core/server/saved_objects/migrations/core/index_migrator.ts delete mode 100644 src/core/server/saved_objects/migrations/core/migration_context.ts delete mode 100644 src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts delete mode 100644 src/core/server/saved_objects/migrations/core/migration_coordinator.ts delete mode 100644 src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts delete mode 100644 src/core/server/saved_objects/migrations/core/migration_es_client.test.ts delete mode 100644 src/core/server/saved_objects/migrations/core/migration_es_client.ts rename src/core/server/saved_objects/migrations/{kibana/__mocks__/kibana_migrator.ts => core/types.ts} (53%) create mode 100644 src/core/server/saved_objects/migrations/core/unused_types.ts rename src/core/server/saved_objects/{migrationsv2 => migrations}/initial_state.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/initial_state.ts (98%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/.gitignore (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/7.7.2_xpack_100k.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/7_13_0_failed_action_tasks.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/7_13_0_transform_failures.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/7_13_0_unknown_types.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.13.0_5k_so_node_01.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.13.0_5k_so_node_02.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.13.0_concurrent_5k_foo.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.13.0_with_corrupted_so.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.13.0_with_unknown_so.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7.7.2_xpack_100k_obj.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/8.0.0_document_migration_failure.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/batch_size_bytes.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/cleanup.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/collects_corrupt_docs.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/corrupt_outdated_docs.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/migration_from_older_v1.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/migration_from_same_v1.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/multiple_es_nodes.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/multiple_kibana_nodes.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/outdated_docs.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/rewriting_id.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/integration_tests/type_registrations.test.ts (100%) delete mode 100644 src/core/server/saved_objects/migrations/kibana/index.ts rename src/core/server/saved_objects/migrations/{kibana => }/kibana_migrator.mock.ts (83%) rename src/core/server/saved_objects/migrations/{kibana => }/kibana_migrator.test.ts (96%) rename src/core/server/saved_objects/migrations/{kibana => }/kibana_migrator.ts (89%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/migrations_state_action_machine.test.ts (99%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/migrations_state_action_machine.ts (99%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/migrations_state_machine_cleanup.mocks.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/migrations_state_machine_cleanup.ts (94%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/create_batches.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/create_batches.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/extract_errors.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/extract_errors.ts (97%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/helpers.ts (98%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/index.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/model.test.ts (99%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/model.ts (99%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/progress.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/progress.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/retry_state.test.ts (99%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/retry_state.ts (97%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/model/types.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/next.test.ts (96%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/next.ts (99%) rename src/core/server/saved_objects/{migrationsv2/index.ts => migrations/run_resilient_migrator.ts} (100%) rename src/core/server/saved_objects/{migrationsv2/types.ts => migrations/state.ts} (96%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/state_action_machine.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/state_action_machine.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/test_helpers/retry.test.ts (100%) rename src/core/server/saved_objects/{migrationsv2 => migrations}/test_helpers/retry_async.ts (100%) delete mode 100644 src/core/server/saved_objects/migrationsv2/README.md delete mode 100644 test/api_integration/apis/saved_objects/migrations.ts diff --git a/src/core/server/saved_objects/migrations/README.md b/src/core/server/saved_objects/migrations/README.md index 69331c3751c8e..60bf84eef87a6 100644 --- a/src/core/server/saved_objects/migrations/README.md +++ b/src/core/server/saved_objects/migrations/README.md @@ -1,222 +1,504 @@ -# Saved Object Migrations +- [Introduction](#introduction) +- [Algorithm steps](#algorithm-steps) + - [INIT](#init) + - [Next action](#next-action) + - [New control state](#new-control-state) + - [CREATE_NEW_TARGET](#create_new_target) + - [Next action](#next-action-1) + - [New control state](#new-control-state-1) + - [LEGACY_SET_WRITE_BLOCK](#legacy_set_write_block) + - [Next action](#next-action-2) + - [New control state](#new-control-state-2) + - [LEGACY_CREATE_REINDEX_TARGET](#legacy_create_reindex_target) + - [Next action](#next-action-3) + - [New control state](#new-control-state-3) + - [LEGACY_REINDEX](#legacy_reindex) + - [Next action](#next-action-4) + - [New control state](#new-control-state-4) + - [LEGACY_REINDEX_WAIT_FOR_TASK](#legacy_reindex_wait_for_task) + - [Next action](#next-action-5) + - [New control state](#new-control-state-5) + - [LEGACY_DELETE](#legacy_delete) + - [Next action](#next-action-6) + - [New control state](#new-control-state-6) + - [WAIT_FOR_YELLOW_SOURCE](#wait_for_yellow_source) + - [Next action](#next-action-7) + - [New control state](#new-control-state-7) + - [SET_SOURCE_WRITE_BLOCK](#set_source_write_block) + - [Next action](#next-action-8) + - [New control state](#new-control-state-8) + - [CREATE_REINDEX_TEMP](#create_reindex_temp) + - [Next action](#next-action-9) + - [New control state](#new-control-state-9) + - [REINDEX_SOURCE_TO_TEMP_OPEN_PIT](#reindex_source_to_temp_open_pit) + - [Next action](#next-action-10) + - [New control state](#new-control-state-10) + - [REINDEX_SOURCE_TO_TEMP_READ](#reindex_source_to_temp_read) + - [Next action](#next-action-11) + - [New control state](#new-control-state-11) + - [REINDEX_SOURCE_TO_TEMP_TRANSFORM](#REINDEX_SOURCE_TO_TEMP_TRANSFORM) + - [Next action](#next-action-12) + - [New control state](#new-control-state-12) + - [REINDEX_SOURCE_TO_TEMP_INDEX_BULK](#reindex_source_to_temp_index_bulk) + - [Next action](#next-action-13) + - [New control state](#new-control-state-13) + - [REINDEX_SOURCE_TO_TEMP_CLOSE_PIT](#reindex_source_to_temp_close_pit) + - [Next action](#next-action-14) + - [New control state](#new-control-state-14) + - [SET_TEMP_WRITE_BLOCK](#set_temp_write_block) + - [Next action](#next-action-15) + - [New control state](#new-control-state-15) + - [CLONE_TEMP_TO_TARGET](#clone_temp_to_target) + - [Next action](#next-action-16) + - [New control state](#new-control-state-16) + - [OUTDATED_DOCUMENTS_SEARCH](#outdated_documents_search) + - [Next action](#next-action-17) + - [New control state](#new-control-state-17) + - [OUTDATED_DOCUMENTS_TRANSFORM](#outdated_documents_transform) + - [Next action](#next-action-18) + - [New control state](#new-control-state-18) + - [UPDATE_TARGET_MAPPINGS](#update_target_mappings) + - [Next action](#next-action-19) + - [New control state](#new-control-state-19) + - [UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK](#update_target_mappings_wait_for_task) + - [Next action](#next-action-20) + - [New control state](#new-control-state-20) + - [MARK_VERSION_INDEX_READY_CONFLICT](#mark_version_index_ready_conflict) + - [Next action](#next-action-21) + - [New control state](#new-control-state-21) +- [Manual QA Test Plan](#manual-qa-test-plan) + - [1. Legacy pre-migration](#1-legacy-pre-migration) + - [2. Plugins enabled/disabled](#2-plugins-enableddisabled) + - [Test scenario 1 (enable a plugin after migration):](#test-scenario-1-enable-a-plugin-after-migration) + - [Test scenario 2 (disable a plugin after migration):](#test-scenario-2-disable-a-plugin-after-migration) + - [Test scenario 3 (multiple instances, enable a plugin after migration):](#test-scenario-3-multiple-instances-enable-a-plugin-after-migration) + - [Test scenario 4 (multiple instances, mixed plugin enabled configs):](#test-scenario-4-multiple-instances-mixed-plugin-enabled-configs) + +# Introduction +In the past, the risk of downtime caused by Kibana's saved object upgrade +migrations have discouraged users from adopting the latest features. v2 +migrations aims to solve this problem by minimizing the operational impact on +our users. + +To achieve this it uses a new migration algorithm where every step of the +algorithm is idempotent. No matter at which step a Kibana instance gets +interrupted, it can always restart the migration from the beginning and repeat +all the steps without requiring any user intervention. This doesn't mean +migrations will never fail, but when they fail for intermittent reasons like +an Elasticsearch cluster running out of heap, Kibana will automatically be +able to successfully complete the migration once the cluster has enough heap. + +For more background information on the problem see the [saved object +migrations +RFC](https://github.com/elastic/kibana/blob/main/rfcs/text/0013_saved_object_migrations.md). + +# Algorithm steps +The design goals for the algorithm was to keep downtime below 10 minutes for +100k saved objects while guaranteeing no data loss and keeping steps as simple +and explicit as possible. + +The algorithm is implemented as a state-action machine based on https://www.microsoft.com/en-us/research/uploads/prod/2016/12/Computation-and-State-Machines.pdf + +The state-action machine defines it's behaviour in steps. Each step is a +transition from a control state s_i to the contral state s_i+1 caused by an +action a_i. -Migrations are the mechanism by which saved object indices are kept up to date with the Kibana system. Plugin authors write their plugins to work with a certain set of mappings, and documents of a certain shape. Migrations ensure that the index actually conforms to those expectations. - -## Migrating the index - -When Kibana boots, prior to serving any requests, it performs a check to see if the kibana index needs to be migrated. - -- If there are out of date docs, or mapping changes, or the current index is not aliased, the index is migrated. -- If the Kibana index does not exist, it is created. - -All of this happens prior to Kibana serving any http requests. - -Here is the gist of what happens if an index migration is necessary: - -* If `.kibana` (or whatever the Kibana index is named) is not an alias, it will be converted to one: - * Reindex `.kibana` into `.kibana_1` - * Delete `.kibana` - * Create an alias `.kibana` that points to `.kibana_1` -* Create a `.kibana_2` index -* Copy all documents from `.kibana_1` into `.kibana_2`, running them through any applicable migrations -* Point the `.kibana` alias to `.kibana_2` - -## Migrating Kibana clusters - -If Kibana is being run in a cluster, migrations will be coordinated so that they only run on one Kibana instance at a time. This is done in a fairly rudimentary way. Let's say we have two Kibana instances, kibana1 and kibana2. - -* kibana1 and kibana2 both start simultaneously and detect that the index requires migration -* kibana1 begins the migration and creates index `.kibana_4` -* kibana2 tries to begin the migration, but fails with the error `.kibana_4 already exists` -* kibana2 logs that it failed to create the migration index, and instead begins polling - * Every few seconds, kibana2 instance checks the `.kibana` index to see if it is done migrating - * Once `.kibana` is determined to be up to date, the kibana2 instance continues booting - -In this example, if the `.kibana_4` index existed prior to Kibana booting, the entire migration process will fail, as all Kibana instances will assume another instance is migrating to the `.kibana_4` index. This problem is only fixable by deleting the `.kibana_4` index. - -## Import / export - -If a user attempts to import FanciPlugin 1.0 documents into a Kibana system that is running FanciPlugin 2.0, those documents will be migrated prior to being persisted in the Kibana index. If a user attempts to import documents having a migration version that is _greater_ than the current Kibana version, the documents will fail to import. - -## Validation - -It might happen that a user modifies their FanciPlugin 1.0 export file to have documents with a migrationVersion of 2.0.0. In this scenario, Kibana will store those documents as if they are up to date, even though they are not, and the result will be unknown, but probably undesirable behavior. - -Similarly, Kibana server APIs assume that they are sent up to date documents unless a document specifies a migrationVersion. This means that out-of-date callers of our APIs will send us out-of-date documents, and those documents will be accepted and stored as if they are up-to-date. - -To prevent this from happening, migration authors should _always_ write a [validation](../validation) function that throws an error if a document is not up to date, and this validation function should always be updated any time a new migration is added for the relevant document types. - -## Document ownership - -In the eyes of the migration system, only one plugin can own a saved object type, or a root-level property on a saved object. - -So, let's say we have a document that looks like this: - -```js -{ - type: 'dashboard', - attributes: { title: 'whatever' }, - securityKey: '324234234kjlke2', -} -``` - -In this document, one plugin might own the `dashboard` type, and another plugin might own the `securityKey` type. If two or more plugins define securityKey migrations `{ migrations: { securityKey: { ... } } }`, Kibana will fail to start. - -To write a migration for this document, the dashboard plugin might look something like this: - -```js -uiExports: { - migrations: { - // This is whatever value your document's "type" field is - dashboard: { - // Takes a pre 1.9.0 dashboard doc, and converts it to 1.9.0 - '1.9.0': (doc) => { - doc.attributes.title = doc.attributes.title.toUpperCase(); - return doc; - }, - - // Takes a 1.9.0 dashboard doc, and converts it to a 2.0.0 - '2.0.0': (doc) => { - doc.attributes.title = doc.attributes.title + '!!!'; - return doc; - }, - }, - }, - // ... normal uiExport stuff -} -``` - -After Kibana migrates the index, our example document would have `{ attributes: { title: 'WHATEVER!!' } }`. - -Each migration function only needs to be able to handle documents belonging to the previous version. The initial migration function (in this example, `1.9.0`) needs to be more flexible, as it may be passed documents of any pre `1.9.0` shape. - -## Disabled plugins - -If a plugin is disabled, all of its documents are retained in the Kibana index. They can be imported and exported. When the plugin is re-enabled, Kibana will migrate any out of date documents that were imported or retained while it was disabled. - -## Configuration - -Kibana index migrations expose a few config settings which might be tweaked: - -* `migrations.scrollDuration` - The - [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html#scroll-search-context) - value used to read batches of documents from the source index. Defaults to - `15m`. -* `migrations.batchSize` - The number of documents to read / transform / write - at a time during index migrations -* `migrations.pollInterval` - How often, in milliseconds, secondary Kibana - instances will poll to see if the primary Kibana instance has finished - migrating the index. -* `migrations.skip` - Skip running migrations on startup (defaults to false). - This should only be used for running integration tests without a running - elasticsearch cluster. Note: even though migrations won't run on startup, - individual docs will still be migrated when read from ES. - -## Example - -To illustrate how migrations work, let's walk through an example, using a fictional plugin: `FanciPlugin`. - -FanciPlugin 1.0 had a mapping that looked like this: - -```js -{ - fanci: { - properties: { - fanciName: { type: 'keyword' }, - }, - }, -} -``` - -But in 2.0, it was decided that `fanciName` should be renamed to `title`. - -So, FanciPlugin 2.0 has a mapping that looks like this: - -```js -{ - fanci: { - properties: { - title: { type: 'keyword' }, - }, - }, -} -``` - -Note, the `fanciName` property is gone altogether. The problem is that lots of people have used FanciPlugin 1.0, and there are lots of documents out in the wild that have the `fanciName` property. FanciPlugin 2.0 won't know how to handle these documents, as it now expects that property to be called `title`. - -To solve this problem, the FanciPlugin authors write a migration which will take all 1.0 documents and transform them into 2.0 documents. - -FanciPlugin's uiExports is modified to have a migrations section that looks like this: - -```js -uiExports: { - migrations: { - // This is whatever value your document's "type" field is - fanci: { - // This is the version of the plugin for which this migration was written, and - // should follow semver conventions. Here, doc is a pre 2.0.0 document which this - // function will modify to have the shape we expect in 2.0.0 - '2.0.0': (doc) => { - const { fanciName } = doc.attributes; - - delete doc.attributes.fanciName; - doc.attributes.title = fanciName; - - return doc; - }, - }, - }, - // ... normal uiExport stuff -} ``` - -Now, whenever Kibana boots, if FanciPlugin is enabled, Kibana scans its index for any documents that have type 'fanci' and have a `migrationVersion.fanci` property that is anything other than `2.0.0`. If any such documents are found, the index is determined to be out of date (or at least of the wrong version), and Kibana attempts to migrate the index. - -At the end of the migration, Kibana's fanci documents will look something like this: - -```js -{ - id: 'someid', - type: 'fanci', - attributes: { - title: 'Shazm!', - }, - migrationVersion: { fanci: '2.0.0' }, -} +s_i -> a_i -> s_i+1 +s_i+1 -> a_i+1 -> s_i+2 ``` -Note, the migrationVersion property has been added, and it contains information about what migrations were applied to the document. - -## Source code - -The migrations source code is grouped into two folders: - -* `core` - Contains index-agnostic, general migration logic, which could be reused for indices other than `.kibana` -* `kibana` - Contains a relatively light-weight wrapper around core, which provides `.kibana` index-specific logic - -Generally, the code eschews classes in favor of functions and basic data structures. The publicly exported code is all class-based, however, in an attempt to conform to Kibana norms. - -### Core - -There are three core entry points. - -* index_migrator - Logic for migrating an index -* document_migrator - Logic for migrating an individual document, used by index_migrator, but also by the saved object client to migrate docs during document creation -* build_active_mappings - Logic to convert mapping properties into a full index mapping object, including the core properties required by any saved object index - -## Testing - -Run Jest tests: - -Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing +Given a control state s1, `next(s1)` returns the next action to execute. +Actions are asynchronous, once the action resolves, we can use the action +response to determine the next state to transition to as defined by the +function `model(state, response)`. +We can then loosely define a step as: ``` -yarn test:jest src/core/server/saved_objects/migrations --watch +s_i+1 = model(s_i, await next(s_i)()) ``` -Run integration tests: +When there are no more actions returned by `next` the state-action machine +terminates such as in the DONE and FATAL control states. + +What follows is a list of all control states. For each control state the +following is described: + - _next action_: the next action triggered by the current control state + - _new control state_: based on the action response, the possible new control states that the machine will transition to + +Since the algorithm runs once for each saved object index the steps below +always reference a single saved object index `.kibana`. When Kibana starts up, +all the steps are also repeated for the `.kibana_task_manager` index but this +is left out of the description for brevity. + +## INIT +### Next action +`fetchIndices` + +Fetch the saved object indices, mappings and aliases to find the source index +and determine whether we’re migrating from a legacy index or a v1 migrations +index. + +### New control state +1. If `.kibana` and the version specific aliases both exists and are pointing +to the same index. This version's migration has already been completed. Since +the same version could have plugins enabled at any time that would introduce +new transforms or mappings. + → `OUTDATED_DOCUMENTS_SEARCH` + +2. If `.kibana` is pointing to an index that belongs to a later version of +Kibana .e.g. a 7.11.0 instance found the `.kibana` alias pointing to +`.kibana_7.12.0_001` fail the migration + → `FATAL` + +3. If the `.kibana` alias exists we’re migrating from either a v1 or v2 index +and the migration source index is the index the `.kibana` alias points to. + → `WAIT_FOR_YELLOW_SOURCE` + +4. If `.kibana` is a concrete index, we’re migrating from a legacy index + → `LEGACY_SET_WRITE_BLOCK` + +5. If there are no `.kibana` indices, this is a fresh deployment. Initialize a + new saved objects index + → `CREATE_NEW_TARGET` + +## CREATE_NEW_TARGET +### Next action +`createIndex` + +Create the target index. This operation is idempotent, if the index already exist, we wait until its status turns yellow + +### New control state + → `MARK_VERSION_INDEX_READY` + +## LEGACY_SET_WRITE_BLOCK +### Next action +`setWriteBlock` + +Set a write block on the legacy index to prevent any older Kibana instances +from writing to the index while the migration is in progress which could cause +lost acknowledged writes. + +This is the first of a series of `LEGACY_*` control states that will: + - reindex the concrete legacy `.kibana` index into a `.kibana_pre6.5.0_001` index + - delete the concrete `.kibana` _index_ so that we're able to create a `.kibana` _alias_ + +### New control state +1. If the write block was successfully added + → `LEGACY_CREATE_REINDEX_TARGET` +2. If the write block failed because the index doesn't exist, it means another instance already completed the legacy pre-migration. Proceed to the next step. + → `LEGACY_CREATE_REINDEX_TARGET` + +## LEGACY_CREATE_REINDEX_TARGET +### Next action +`createIndex` + +Create a new `.kibana_pre6.5.0_001` index into which we can reindex the legacy +index. (Since the task manager index was converted from a data index into a +saved objects index in 7.4 it will be reindexed into `.kibana_pre7.4.0_001`) +### New control state + → `LEGACY_REINDEX` + +## LEGACY_REINDEX +### Next action +`reindex` + +Let Elasticsearch reindex the legacy index into `.kibana_pre6.5.0_001`. (For +the task manager index we specify a `preMigrationScript` to convert the +original task manager documents into valid saved objects) +### New control state + → `LEGACY_REINDEX_WAIT_FOR_TASK` + + +## LEGACY_REINDEX_WAIT_FOR_TASK +### Next action +`waitForReindexTask` + +Wait for up to 60s for the reindex task to complete. +### New control state +1. If the reindex task completed + → `LEGACY_DELETE` +2. If the reindex task failed with a `target_index_had_write_block` or + `index_not_found_exception` another instance already completed this step + → `LEGACY_DELETE` +3. If the reindex task is still in progress + → `LEGACY_REINDEX_WAIT_FOR_TASK` + +## LEGACY_DELETE +### Next action +`updateAliases` + +Use the updateAliases API to atomically remove the legacy index and create a +new `.kibana` alias that points to `.kibana_pre6.5.0_001`. +### New control state +1. If the action succeeds + → `SET_SOURCE_WRITE_BLOCK` +2. If the action fails with `remove_index_not_a_concrete_index` or + `index_not_found_exception` another instance has already completed this step. + → `SET_SOURCE_WRITE_BLOCK` + +## WAIT_FOR_YELLOW_SOURCE +### Next action +`waitForIndexStatusYellow` + +Wait for the Elasticsearch cluster to be in "yellow" state. It means the index's primary shard is allocated and the index is ready for searching/indexing documents, but ES wasn't able to allocate the replicas. +We don't have as much data redundancy as we could have, but it's enough to start the migration. + +### New control state + → `SET_SOURCE_WRITE_BLOCK` + +## SET_SOURCE_WRITE_BLOCK +### Next action +`setWriteBlock` + +Set a write block on the source index to prevent any older Kibana instances from writing to the index while the migration is in progress which could cause lost acknowledged writes. + +### New control state + → `CREATE_REINDEX_TEMP` + +## CREATE_REINDEX_TEMP +### Next action +`createIndex` + +This operation is idempotent, if the index already exist, we wait until its status turns yellow. + +- Because we will be transforming documents before writing them into this index, we can already set the mappings to the target mappings for this version. The source index might contain documents belonging to a disabled plugin. So set `dynamic: false` mappings for any unknown saved object types. +- (Since we never query the temporary index we can potentially disable refresh to speed up indexing performance. Profile to see if gains justify complexity) + +### New control state + → `REINDEX_SOURCE_TO_TEMP_OPEN_PIT` + +## REINDEX_SOURCE_TO_TEMP_OPEN_PIT +### Next action +`openPIT` + +Open a PIT. Since there is a write block on the source index there is basically no overhead to keeping the PIT so we can lean towards a larger `keep_alive` value like 10 minutes. +### New control state + → `REINDEX_SOURCE_TO_TEMP_READ` + +## REINDEX_SOURCE_TO_TEMP_READ +### Next action +`readNextBatchOfSourceDocuments` + +Read the next batch of outdated documents from the source index by using search after with our PIT. + +### New control state +1. If the batch contained > 0 documents + → `REINDEX_SOURCE_TO_TEMP_TRANSFORM` +2. If there are no more documents returned + → `REINDEX_SOURCE_TO_TEMP_CLOSE_PIT` + +## REINDEX_SOURCE_TO_TEMP_TRANSFORM +### Next action +`transformRawDocs` + +Transform the current batch of documents + +In order to support sharing saved objects to multiple spaces in 8.0, the +transforms will also regenerate document `_id`'s. To ensure that this step +remains idempotent, the new `_id` is deterministically generated using UUIDv5 +ensuring that each Kibana instance generates the same new `_id` for the same document. +### New control state + → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` +## REINDEX_SOURCE_TO_TEMP_INDEX_BULK +### Next action +`bulkIndexTransformedDocuments` + +Use the bulk API create action to write a batch of up-to-date documents. The +create action ensures that there will be only one write per reindexed document +even if multiple Kibana instances are performing this step. Use +`refresh=false` to speed up the create actions, the `UPDATE_TARGET_MAPPINGS` +step will ensure that the index is refreshed before we start serving traffic. + +The following errors are ignored because it means another instance already +completed this step: + - documents already exist in the temp index + - temp index has a write block + - temp index is not found +### New control state +1. If `currentBatch` is the last batch in `transformedDocBatches` + → `REINDEX_SOURCE_TO_TEMP_READ` +2. If there are more batches left in `transformedDocBatches` + → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` + +## REINDEX_SOURCE_TO_TEMP_CLOSE_PIT +### Next action +`closePIT` + +### New control state + → `SET_TEMP_WRITE_BLOCK` + +## SET_TEMP_WRITE_BLOCK +### Next action +`setWriteBlock` + +Set a write block on the temporary index so that we can clone it. +### New control state + → `CLONE_TEMP_TO_TARGET` + +## CLONE_TEMP_TO_TARGET +### Next action +`cloneIndex` + +Ask elasticsearch to clone the temporary index into the target index. If the target index already exists (because another node already started the clone operation), wait until the clone is complete by waiting for a yellow index status. + +We can’t use the temporary index as our target index because one instance can complete the migration, delete a document, and then a second instance starts the reindex operation and re-creates the deleted document. By cloning the temporary index and only accepting writes/deletes from the cloned target index, we prevent lost acknowledged deletes. + +### New control state + → `OUTDATED_DOCUMENTS_SEARCH` + +## OUTDATED_DOCUMENTS_SEARCH +### Next action +`searchForOutdatedDocuments` + +Search for outdated saved object documents. Will return one batch of +documents. + +If another instance has a disabled plugin it will reindex that plugin's +documents without transforming them. Because this instance doesn't know which +plugins were disabled by the instance that performed the +`REINDEX_SOURCE_TO_TEMP_TRANSFORM` step, we need to search for outdated documents +and transform them to ensure that everything is up to date. + +### New control state +1. Found outdated documents? + → `OUTDATED_DOCUMENTS_TRANSFORM` +2. All documents up to date + → `UPDATE_TARGET_MAPPINGS` + +## OUTDATED_DOCUMENTS_TRANSFORM +### Next action +`transformRawDocs` + `bulkOverwriteTransformedDocuments` + +Once transformed we use an index operation to overwrite the outdated document with the up-to-date version. Optimistic concurrency control ensures that we only overwrite the document once so that any updates/writes by another instance which already completed the migration aren’t overwritten and lost. + +### New control state + → `OUTDATED_DOCUMENTS_SEARCH` + +## UPDATE_TARGET_MAPPINGS +### Next action +`updateAndPickupMappings` + +If another instance has some plugins disabled it will disable the mappings of that plugin's types when creating the temporary index. This action will +update the mappings and then use an update_by_query to ensure that all fields are “picked-up” and ready to be searched over. + +### New control state + → `UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK` + +## UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK +### Next action +`updateAliases` + +Atomically apply the `versionIndexReadyActions` using the _alias actions API. By performing the following actions we guarantee that if multiple versions of Kibana started the upgrade in parallel, only one version will succeed. + +1. verify that the current alias is still pointing to the source index +2. Point the version alias and the current alias to the target index. +3. Remove the temporary index + +### New control state +1. If all the actions succeed we’re ready to serve traffic + → `DONE` +2. If action (1) fails with alias_not_found_exception or action (3) fails with index_not_found_exception another instance already completed the migration + → `MARK_VERSION_INDEX_READY_CONFLICT` + +## MARK_VERSION_INDEX_READY_CONFLICT +### Next action +`fetchIndices` + +Fetch the saved object indices + +### New control state +If another instance completed a migration from the same source we need to verify that it is running the same version. + +1. If the current and version aliases are pointing to the same index the instance that completed the migration was on the same version and it’s safe to start serving traffic. + → `DONE` +2. If the other instance was running a different version we fail the migration. Once we restart one of two things can happen: the other instance is an older version and we will restart the migration, or, it’s a newer version and we will refuse to start up. + → `FATAL` + +# Manual QA Test Plan +## 1. Legacy pre-migration +When upgrading from a legacy index additional steps are required before the +regular migration process can start. + +We have the following potential legacy indices: + - v5.x index that wasn't upgraded -> kibana should refuse to start the migration + - v5.x index that was upgraded to v6.x: `.kibana-6` _index_ with `.kibana` _alias_ + - < v6.5 `.kibana` _index_ (Saved Object Migrations were + introduced in v6.5 https://github.com/elastic/kibana/pull/20243) + - TODO: Test versions which introduced the `kibana_index_template` template? + - < v7.4 `.kibana_task_manager` _index_ (Task Manager started + using Saved Objects in v7.4 https://github.com/elastic/kibana/pull/39829) + +Test plan: +1. Ensure that the different versions of Kibana listed above can successfully + upgrade to 7.11. +2. Ensure that multiple Kibana nodes can migrate a legacy index in parallel + (choose a representative legacy version to test with e.g. v6.4). Add a lot + of Saved Objects to Kibana to increase the time it takes for a migration to + complete which will make it easier to introduce failures. + 1. If all instances are started in parallel the upgrade should succeed + 2. If nodes are randomly restarted shortly after they start participating + in the migration the upgrade should either succeed or never complete. + However, if a fatal error occurs it should never result in permanent + failure. + 1. Start one instance, wait 500 ms + 2. Start a second instance + 3. If an instance starts a saved object migration, wait X ms before + killing the process and restarting the migration. + 4. Keep decreasing X until migrations are barely able to complete. + 5. If a migration fails with a fatal error, start a Kibana that doesn't + get restarted. Given enough time, it should always be able to + successfully complete the migration. + +For a successful migration the following behaviour should be observed: + 1. The `.kibana` index should be reindexed into a `.kibana_pre6.5.0` index + 2. The `.kibana` index should be deleted + 3. The `.kibana_index_template` should be deleted + 4. The `.kibana_pre6.5.0` index should have a write block applied + 5. Documents from `.kibana_pre6.5.0` should be migrated into `.kibana_7.11.0_001` + 6. Once migration has completed, the `.kibana_current` and `.kibana_7.11.0` + aliases should point to the `.kibana_7.11.0_001` index. + +## 2. Plugins enabled/disabled +Kibana plugins can be disabled/enabled at any point in time. We need to ensure +that Saved Object documents are migrated for all the possible sequences of +enabling, disabling, before or after a version upgrade. + +### Test scenario 1 (enable a plugin after migration): +1. Start an old version of Kibana (< 7.11) +2. Create a document that we know will be migrated in a later version (i.e. + create a `dashboard`) +3. Disable the plugin to which the document belongs (i.e `dashboard` plugin) +4. Upgrade Kibana to v7.11 making sure the plugin in step (3) is still disabled. +5. Enable the plugin from step (3) +6. Restart Kibana +7. Ensure that the document from step (2) has been migrated + (`migrationVersion` contains 7.11.0) + +### Test scenario 2 (disable a plugin after migration): +1. Start an old version of Kibana (< 7.11) +2. Create a document that we know will be migrated in a later version (i.e. + create a `dashboard`) +3. Upgrade Kibana to v7.11 making sure the plugin in step (3) is enabled. +4. Disable the plugin to which the document belongs (i.e `dashboard` plugin) +6. Restart Kibana +7. Ensure that Kibana logs a warning, but continues to start even though there + are saved object documents which don't belong to an enable plugin + +### Test scenario 3 (multiple instances, enable a plugin after migration): +Follow the steps from 'Test scenario 1', but perform the migration with +multiple instances of Kibana + +### Test scenario 4 (multiple instances, mixed plugin enabled configs): +We don't support this upgrade scenario, but it's worth making sure we don't +have data loss when there's a user error. +1. Start an old version of Kibana (< 7.11) +2. Create a document that we know will be migrated in a later version (i.e. + create a `dashboard`) +3. Disable the plugin to which the document belongs (i.e `dashboard` plugin) +4. Upgrade Kibana to v7.11 using multiple instances of Kibana. The plugin from + step (3) should be enabled on half of the instances and disabled on the + other half. +5. Ensure that the document from step (2) has been migrated + (`migrationVersion` contains 7.11.0) -``` -node scripts/functional_tests_server -node scripts/functional_test_runner --config test/api_integration/config.js --grep migration -``` diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/kibana_migrator.test.ts.snap similarity index 100% rename from src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap rename to src/core/server/saved_objects/migrations/__snapshots__/kibana_migrator.test.ts.snap diff --git a/src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap similarity index 100% rename from src/core/server/saved_objects/migrationsv2/__snapshots__/migrations_state_action_machine.test.ts.snap rename to src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap diff --git a/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.test.ts b/src/core/server/saved_objects/migrations/actions/bulk_overwrite_transformed_documents.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.test.ts rename to src/core/server/saved_objects/migrations/actions/bulk_overwrite_transformed_documents.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts b/src/core/server/saved_objects/migrations/actions/bulk_overwrite_transformed_documents.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts rename to src/core/server/saved_objects/migrations/actions/bulk_overwrite_transformed_documents.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/calculate_exclude_filters.test.ts b/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/calculate_exclude_filters.test.ts rename to src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/calculate_exclude_filters.ts b/src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/calculate_exclude_filters.ts rename to src/core/server/saved_objects/migrations/actions/calculate_exclude_filters.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.test.ts b/src/core/server/saved_objects/migrations/actions/catch_retryable_es_client_errors.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.test.ts rename to src/core/server/saved_objects/migrations/actions/catch_retryable_es_client_errors.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.ts b/src/core/server/saved_objects/migrations/actions/catch_retryable_es_client_errors.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.ts rename to src/core/server/saved_objects/migrations/actions/catch_retryable_es_client_errors.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts rename to src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts b/src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts rename to src/core/server/saved_objects/migrations/actions/check_for_unknown_docs.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/clone_index.test.ts b/src/core/server/saved_objects/migrations/actions/clone_index.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/clone_index.test.ts rename to src/core/server/saved_objects/migrations/actions/clone_index.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/clone_index.ts b/src/core/server/saved_objects/migrations/actions/clone_index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/clone_index.ts rename to src/core/server/saved_objects/migrations/actions/clone_index.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/close_pit.test.ts b/src/core/server/saved_objects/migrations/actions/close_pit.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/close_pit.test.ts rename to src/core/server/saved_objects/migrations/actions/close_pit.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/close_pit.ts b/src/core/server/saved_objects/migrations/actions/close_pit.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/close_pit.ts rename to src/core/server/saved_objects/migrations/actions/close_pit.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/constants.ts b/src/core/server/saved_objects/migrations/actions/constants.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/constants.ts rename to src/core/server/saved_objects/migrations/actions/constants.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/create_index.test.ts b/src/core/server/saved_objects/migrations/actions/create_index.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/create_index.test.ts rename to src/core/server/saved_objects/migrations/actions/create_index.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/create_index.ts b/src/core/server/saved_objects/migrations/actions/create_index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/create_index.ts rename to src/core/server/saved_objects/migrations/actions/create_index.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/es_errors.test.ts b/src/core/server/saved_objects/migrations/actions/es_errors.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/es_errors.test.ts rename to src/core/server/saved_objects/migrations/actions/es_errors.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/es_errors.ts b/src/core/server/saved_objects/migrations/actions/es_errors.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/es_errors.ts rename to src/core/server/saved_objects/migrations/actions/es_errors.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/fetch_indices.test.ts b/src/core/server/saved_objects/migrations/actions/fetch_indices.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/fetch_indices.test.ts rename to src/core/server/saved_objects/migrations/actions/fetch_indices.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/fetch_indices.ts b/src/core/server/saved_objects/migrations/actions/fetch_indices.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/fetch_indices.ts rename to src/core/server/saved_objects/migrations/actions/fetch_indices.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrations/actions/index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/index.ts rename to src/core/server/saved_objects/migrations/actions/index.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts rename to src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts index b85fb0257d15c..1b6a668fe57fd 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts @@ -39,7 +39,7 @@ import { import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; import { errors } from '@elastic/elasticsearch'; -import { DocumentsTransformFailed, DocumentsTransformSuccess } from '../../../migrations/core'; +import { DocumentsTransformFailed, DocumentsTransformSuccess } from '../../core'; import { TaskEither } from 'fp-ts/lib/TaskEither'; import Path from 'path'; diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip b/src/core/server/saved_objects/migrations/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip rename to src/core/server/saved_objects/migrations/actions/integration_tests/archives/7.7.2_xpack_100k_obj.zip diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/es_errors.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/es_errors.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/integration_tests/es_errors.test.ts rename to src/core/server/saved_objects/migrations/actions/integration_tests/es_errors.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/open_pit.test.ts b/src/core/server/saved_objects/migrations/actions/open_pit.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/open_pit.test.ts rename to src/core/server/saved_objects/migrations/actions/open_pit.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/open_pit.ts b/src/core/server/saved_objects/migrations/actions/open_pit.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/open_pit.ts rename to src/core/server/saved_objects/migrations/actions/open_pit.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/pickup_updated_mappings.test.ts b/src/core/server/saved_objects/migrations/actions/pickup_updated_mappings.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/pickup_updated_mappings.test.ts rename to src/core/server/saved_objects/migrations/actions/pickup_updated_mappings.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/pickup_updated_mappings.ts b/src/core/server/saved_objects/migrations/actions/pickup_updated_mappings.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/pickup_updated_mappings.ts rename to src/core/server/saved_objects/migrations/actions/pickup_updated_mappings.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/read_with_pit.test.ts b/src/core/server/saved_objects/migrations/actions/read_with_pit.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/read_with_pit.test.ts rename to src/core/server/saved_objects/migrations/actions/read_with_pit.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/read_with_pit.ts b/src/core/server/saved_objects/migrations/actions/read_with_pit.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/read_with_pit.ts rename to src/core/server/saved_objects/migrations/actions/read_with_pit.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/refresh_index.test.ts b/src/core/server/saved_objects/migrations/actions/refresh_index.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/refresh_index.test.ts rename to src/core/server/saved_objects/migrations/actions/refresh_index.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/refresh_index.ts b/src/core/server/saved_objects/migrations/actions/refresh_index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/refresh_index.ts rename to src/core/server/saved_objects/migrations/actions/refresh_index.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/reindex.test.ts b/src/core/server/saved_objects/migrations/actions/reindex.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/reindex.test.ts rename to src/core/server/saved_objects/migrations/actions/reindex.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/reindex.ts b/src/core/server/saved_objects/migrations/actions/reindex.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/reindex.ts rename to src/core/server/saved_objects/migrations/actions/reindex.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/remove_write_block.test.ts b/src/core/server/saved_objects/migrations/actions/remove_write_block.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/remove_write_block.test.ts rename to src/core/server/saved_objects/migrations/actions/remove_write_block.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/remove_write_block.ts b/src/core/server/saved_objects/migrations/actions/remove_write_block.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/remove_write_block.ts rename to src/core/server/saved_objects/migrations/actions/remove_write_block.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/search_for_outdated_documents.test.ts b/src/core/server/saved_objects/migrations/actions/search_for_outdated_documents.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/search_for_outdated_documents.test.ts rename to src/core/server/saved_objects/migrations/actions/search_for_outdated_documents.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/search_for_outdated_documents.ts b/src/core/server/saved_objects/migrations/actions/search_for_outdated_documents.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/search_for_outdated_documents.ts rename to src/core/server/saved_objects/migrations/actions/search_for_outdated_documents.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/set_write_block.test.ts b/src/core/server/saved_objects/migrations/actions/set_write_block.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/set_write_block.test.ts rename to src/core/server/saved_objects/migrations/actions/set_write_block.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/set_write_block.ts b/src/core/server/saved_objects/migrations/actions/set_write_block.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/set_write_block.ts rename to src/core/server/saved_objects/migrations/actions/set_write_block.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/transform_docs.ts b/src/core/server/saved_objects/migrations/actions/transform_docs.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/transform_docs.ts rename to src/core/server/saved_objects/migrations/actions/transform_docs.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/update_aliases.test.ts b/src/core/server/saved_objects/migrations/actions/update_aliases.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/update_aliases.test.ts rename to src/core/server/saved_objects/migrations/actions/update_aliases.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/update_aliases.ts b/src/core/server/saved_objects/migrations/actions/update_aliases.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/update_aliases.ts rename to src/core/server/saved_objects/migrations/actions/update_aliases.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/update_and_pickup_mappings.test.ts b/src/core/server/saved_objects/migrations/actions/update_and_pickup_mappings.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/update_and_pickup_mappings.test.ts rename to src/core/server/saved_objects/migrations/actions/update_and_pickup_mappings.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/update_and_pickup_mappings.ts b/src/core/server/saved_objects/migrations/actions/update_and_pickup_mappings.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/update_and_pickup_mappings.ts rename to src/core/server/saved_objects/migrations/actions/update_and_pickup_mappings.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/verify_reindex.ts b/src/core/server/saved_objects/migrations/actions/verify_reindex.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/verify_reindex.ts rename to src/core/server/saved_objects/migrations/actions/verify_reindex.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.test.ts b/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.test.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts b/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_index_status_yellow.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_pickup_updated_mappings_task.test.ts b/src/core/server/saved_objects/migrations/actions/wait_for_pickup_updated_mappings_task.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_pickup_updated_mappings_task.test.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_pickup_updated_mappings_task.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_pickup_updated_mappings_task.ts b/src/core/server/saved_objects/migrations/actions/wait_for_pickup_updated_mappings_task.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_pickup_updated_mappings_task.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_pickup_updated_mappings_task.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_reindex_task.test.ts b/src/core/server/saved_objects/migrations/actions/wait_for_reindex_task.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_reindex_task.test.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_reindex_task.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_reindex_task.ts b/src/core/server/saved_objects/migrations/actions/wait_for_reindex_task.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_reindex_task.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_reindex_task.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_task.test.ts b/src/core/server/saved_objects/migrations/actions/wait_for_task.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_task.test.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_task.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/actions/wait_for_task.ts b/src/core/server/saved_objects/migrations/actions/wait_for_task.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/actions/wait_for_task.ts rename to src/core/server/saved_objects/migrations/actions/wait_for_task.ts diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap deleted file mode 100644 index 6bd567be204d0..0000000000000 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ElasticIndex write writes documents in bulk to the index 1`] = ` -Array [ - Object { - "body": Array [ - Object { - "index": Object { - "_id": "niceguy:fredrogers", - "_index": ".myalias", - }, - }, - Object { - "niceguy": Object { - "aka": "Mr Rogers", - }, - "quotes": Array [ - "The greatest gift you ever give is your honest self.", - ], - "type": "niceguy", - }, - Object { - "index": Object { - "_id": "badguy:rickygervais", - "_index": ".myalias", - }, - }, - Object { - "badguy": Object { - "aka": "Dominic Badguy", - }, - "migrationVersion": Object { - "badguy": "2.3.4", - }, - "type": "badguy", - }, - ], - }, -] -`; diff --git a/src/core/server/saved_objects/migrations/core/call_cluster.ts b/src/core/server/saved_objects/migrations/core/call_cluster.ts deleted file mode 100644 index 156689c8d96f9..0000000000000 --- a/src/core/server/saved_objects/migrations/core/call_cluster.ts +++ /dev/null @@ -1,28 +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. - */ - -/** - * This file is nothing more than type signatures for the subset of - * elasticsearch.js that migrations use. There is no actual logic / - * funcationality contained here. - */ - -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -export type AliasAction = - | { - remove_index: { index: string }; - } - | { remove: { index: string; alias: string } } - | { add: { index: string; alias: string } }; - -export interface RawDoc { - _id: estypes.Id; - _source: any; - _type?: string; -} diff --git a/src/core/server/saved_objects/migrations/core/migration_context.test.ts b/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.test.ts similarity index 96% rename from src/core/server/saved_objects/migrations/core/migration_context.test.ts rename to src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.test.ts index 0ca858c34e8ba..1cf77069e1e4d 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.test.ts +++ b/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { disableUnknownTypeMappingFields } from './migration_context'; +import { disableUnknownTypeMappingFields } from './disable_unknown_type_mapping_fields'; describe('disableUnknownTypeMappingFields', () => { const sourceMappings = { diff --git a/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.ts b/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.ts new file mode 100644 index 0000000000000..d11e3a40df8d8 --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/disable_unknown_type_mapping_fields.ts @@ -0,0 +1,60 @@ +/* + * 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 { SavedObjectsMappingProperties, IndexMapping } from '../../mappings'; + +/** + * Merges the active mappings and the source mappings while disabling the + * fields of any unknown Saved Object types present in the source index's + * mappings. + * + * Since the Saved Objects index has `dynamic: strict` defined at the + * top-level, only Saved Object types for which a mapping exists can be + * inserted into the index. To ensure that we can continue to store Saved + * Object documents belonging to a disabled plugin we define a mapping for all + * the unknown Saved Object types that were present in the source index's + * mappings. To limit the field count as much as possible, these unkwnown + * type's mappings are set to `dynamic: false`. + * + * (Since we're using the source index mappings instead of looking at actual + * document types in the inedx, we potentially add more "unknown types" than + * what would be necessary to support migrating all the data over to the + * target index.) + * + * @param activeMappings The mappings compiled from all the Saved Object types + * known to this Kibana node. + * @param sourceMappings The mappings of index used as the migration source. + * @returns The mappings that should be applied to the target index. + */ +export function disableUnknownTypeMappingFields( + activeMappings: IndexMapping, + sourceMappings: IndexMapping +): IndexMapping { + const targetTypes = Object.keys(activeMappings.properties); + + const disabledTypesProperties = Object.keys(sourceMappings.properties ?? {}) + .filter((sourceType) => { + const isObjectType = 'properties' in sourceMappings.properties[sourceType]; + // Only Object/Nested datatypes can be excluded from the field count by + // using `dynamic: false`. + return !targetTypes.includes(sourceType) && isObjectType; + }) + .reduce((disabledTypesAcc, sourceType) => { + disabledTypesAcc[sourceType] = { dynamic: false, properties: {} }; + return disabledTypesAcc; + }, {} as SavedObjectsMappingProperties); + + return { + ...activeMappings, + properties: { + ...sourceMappings.properties, + ...disabledTypesProperties, + ...activeMappings.properties, + }, + }; +} diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts deleted file mode 100644 index 2cdeb479f50f9..0000000000000 --- a/src/core/server/saved_objects/migrations/core/elastic_index.test.ts +++ /dev/null @@ -1,702 +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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import _ from 'lodash'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; -import * as Index from './elastic_index'; - -describe('ElasticIndex', () => { - let client: ReturnType; - - beforeEach(() => { - client = elasticsearchClientMock.createElasticsearchClient(); - }); - describe('fetchInfo', () => { - test('it handles 404', async () => { - client.indices.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - - const info = await Index.fetchInfo(client, '.kibana-test'); - expect(info).toEqual({ - aliases: {}, - exists: false, - indexName: '.kibana-test', - mappings: { dynamic: 'strict', properties: {} }, - }); - - expect(client.indices.get).toHaveBeenCalledWith({ index: '.kibana-test' }, { ignore: [404] }); - }); - - test('decorates index info with exists and indexName', async () => { - client.indices.get.mockImplementation((params) => { - const index = params!.index as string; - return elasticsearchClientMock.createSuccessTransportRequestPromise({ - [index]: { - aliases: { foo: index }, - mappings: { dynamic: 'strict', properties: { a: 'b' } as any }, - settings: {}, - }, - } as estypes.IndicesGetResponse); - }); - - const info = await Index.fetchInfo(client, '.baz'); - expect(info).toEqual({ - aliases: { foo: '.baz' }, - mappings: { dynamic: 'strict', properties: { a: 'b' } }, - exists: true, - indexName: '.baz', - settings: {}, - }); - }); - }); - - describe('createIndex', () => { - test('calls indices.create', async () => { - await Index.createIndex(client, '.abcd', { foo: 'bar' } as any); - - expect(client.indices.create).toHaveBeenCalledTimes(1); - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { foo: 'bar' }, - settings: { - auto_expand_replicas: '0-1', - number_of_shards: 1, - }, - }, - index: '.abcd', - }); - }); - }); - - describe('claimAlias', () => { - test('handles unaliased indices', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - - await Index.claimAlias(client, '.hola-42', '.hola'); - - expect(client.indices.getAlias).toHaveBeenCalledWith( - { - name: '.hola', - }, - { ignore: [404] } - ); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [{ add: { index: '.hola-42', alias: '.hola' } }], - }, - }); - expect(client.indices.refresh).toHaveBeenCalledWith({ - index: '.hola-42', - }); - }); - - test('removes existing alias', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': { aliases: { '.muchacha': {} } }, - }) - ); - - await Index.claimAlias(client, '.ze-index', '.muchacha'); - - expect(client.indices.getAlias).toHaveBeenCalledTimes(1); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [ - { remove: { index: '.my-fanci-index', alias: '.muchacha' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }, - }); - expect(client.indices.refresh).toHaveBeenCalledWith({ - index: '.ze-index', - }); - }); - - test('allows custom alias actions', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': { aliases: { '.muchacha': {} } }, - }) - ); - - await Index.claimAlias(client, '.ze-index', '.muchacha', [ - { remove_index: { index: 'awww-snap!' } }, - ]); - - expect(client.indices.getAlias).toHaveBeenCalledTimes(1); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [ - { remove_index: { index: 'awww-snap!' } }, - { remove: { index: '.my-fanci-index', alias: '.muchacha' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }, - }); - expect(client.indices.refresh).toHaveBeenCalledWith({ - index: '.ze-index', - }); - }); - }); - - describe('convertToAlias', () => { - test('it creates the destination index, then reindexes to it', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': { aliases: { '.muchacha': {} } }, - }) - ); - client.reindex.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - task: 'abc', - } as estypes.ReindexResponse) - ); - client.tasks.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - completed: true, - } as estypes.TasksGetResponse) - ); - - const info = { - aliases: {}, - exists: true, - indexName: '.ze-index', - mappings: { - dynamic: 'strict' as const, - properties: { foo: { type: 'keyword' } }, - }, - } as const; - - await Index.convertToAlias( - client, - info, - '.muchacha', - 10, - `ctx._id = ctx._source.type + ':' + ctx._id` - ); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - properties: { foo: { type: 'keyword' } }, - }, - settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, - }, - index: '.ze-index', - }); - - expect(client.reindex).toHaveBeenCalledWith({ - body: { - dest: { index: '.ze-index' }, - source: { index: '.muchacha', size: 10 }, - script: { - source: `ctx._id = ctx._source.type + ':' + ctx._id`, - lang: 'painless', - }, - }, - refresh: true, - wait_for_completion: false, - }); - - expect(client.tasks.get).toHaveBeenCalledWith({ - task_id: 'abc', - }); - - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [ - { remove_index: { index: '.muchacha' } }, - { remove: { alias: '.muchacha', index: '.my-fanci-index' } }, - { add: { index: '.ze-index', alias: '.muchacha' } }, - ], - }, - }); - - expect(client.indices.refresh).toHaveBeenCalledWith({ - index: '.ze-index', - }); - }); - - test('throws error if re-index task fails', async () => { - client.indices.getAlias.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - '.my-fanci-index': { aliases: { '.muchacha': {} } }, - }) - ); - client.reindex.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - task: 'abc', - } as estypes.ReindexResponse) - ); - client.tasks.get.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch GetTaskResponse requires a `task` property even on errors - elasticsearchClientMock.createSuccessTransportRequestPromise({ - completed: true, - error: { - type: 'search_phase_execution_exception', - reason: 'all shards failed', - failed_shards: [], - }, - } as estypes.TasksGetResponse) - ); - - const info = { - aliases: {}, - exists: true, - indexName: '.ze-index', - mappings: { - dynamic: 'strict', - properties: { foo: { type: 'keyword' } }, - }, - }; - - // @ts-expect-error - await expect(Index.convertToAlias(client, info, '.muchacha', 10)).rejects.toThrow( - /Re-index failed \[search_phase_execution_exception\] all shards failed/ - ); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - properties: { foo: { type: 'keyword' } }, - }, - settings: { auto_expand_replicas: '0-1', number_of_shards: 1 }, - }, - index: '.ze-index', - }); - - expect(client.reindex).toHaveBeenCalledWith({ - body: { - dest: { index: '.ze-index' }, - source: { index: '.muchacha', size: 10 }, - }, - refresh: true, - wait_for_completion: false, - }); - - expect(client.tasks.get).toHaveBeenCalledWith({ - task_id: 'abc', - }); - }); - }); - - describe('write', () => { - test('writes documents in bulk to the index', async () => { - client.bulk.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - items: [] as any[], - } as estypes.BulkResponse) - ); - - const index = '.myalias'; - const docs = [ - { - _id: 'niceguy:fredrogers', - _source: { - type: 'niceguy', - niceguy: { - aka: 'Mr Rogers', - }, - quotes: ['The greatest gift you ever give is your honest self.'], - }, - }, - { - _id: 'badguy:rickygervais', - _source: { - type: 'badguy', - badguy: { - aka: 'Dominic Badguy', - }, - migrationVersion: { badguy: '2.3.4' }, - }, - }, - ]; - - await Index.write(client, index, docs); - - expect(client.bulk).toHaveBeenCalled(); - expect(client.bulk.mock.calls[0]).toMatchSnapshot(); - }); - - test('fails if any document fails', async () => { - client.bulk.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - items: [{ index: { error: { type: 'shazm', reason: 'dern' } } }], - } as estypes.BulkResponse) - ); - - const index = '.myalias'; - const docs = [ - { - _id: 'niceguy:fredrogers', - _source: { - type: 'niceguy', - niceguy: { - aka: 'Mr Rogers', - }, - }, - }, - ]; - - await expect(Index.write(client as any, index, docs)).rejects.toThrow(/dern/); - expect(client.bulk).toHaveBeenCalledTimes(1); - }); - }); - - describe('reader', () => { - test('returns docs in batches', async () => { - const index = '.myalias'; - const batch1 = [ - { - _id: 'such:1', - _source: { type: 'such', such: { num: 1 } }, - }, - ]; - const batch2 = [ - { - _id: 'aaa:2', - _source: { type: 'aaa', aaa: { num: 2 } }, - }, - { - _id: 'bbb:3', - _source: { - bbb: { num: 3 }, - migrationVersion: { bbb: '3.2.5' }, - type: 'bbb', - }, - }, - ]; - - client.search = jest.fn().mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'x', - _shards: { success: 1, total: 1 }, - hits: { hits: _.cloneDeep(batch1) }, - }) - ); - client.scroll = jest - .fn() - .mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'y', - _shards: { success: 1, total: 1 }, - hits: { hits: _.cloneDeep(batch2) }, - }) - ) - .mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'z', - _shards: { success: 1, total: 1 }, - hits: { hits: [] }, - }) - ); - - const read = Index.reader(client, index, { batchSize: 100, scrollDuration: '5m' }); - - expect(await read()).toEqual(batch1); - expect(await read()).toEqual(batch2); - expect(await read()).toEqual([]); - - expect(client.search).toHaveBeenCalledWith({ - body: { - size: 100, - query: Index.excludeUnusedTypesQuery, - }, - index, - scroll: '5m', - }); - expect(client.scroll).toHaveBeenCalledWith({ - scroll: '5m', - scroll_id: 'x', - }); - expect(client.scroll).toHaveBeenCalledWith({ - scroll: '5m', - scroll_id: 'y', - }); - expect(client.clearScroll).toHaveBeenCalledWith({ - scroll_id: 'z', - }); - }); - - test('returns all root-level properties', async () => { - const index = '.myalias'; - const batch = [ - { - _id: 'such:1', - _source: { - acls: '3230a', - foos: { is: 'fun' }, - such: { num: 1 }, - type: 'such', - }, - }, - ]; - - client.search = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'x', - _shards: { success: 1, total: 1 }, - hits: { hits: _.cloneDeep(batch) }, - }) - ); - client.scroll = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'z', - _shards: { success: 1, total: 1 }, - hits: { hits: [] }, - }) - ); - - const read = Index.reader(client, index, { - batchSize: 100, - scrollDuration: '5m', - }); - - expect(await read()).toEqual(batch); - }); - - test('fails if not all shards were successful', async () => { - const index = '.myalias'; - - client.search = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _shards: { successful: 1, total: 2 }, - }) - ); - - const read = Index.reader(client, index, { - batchSize: 100, - scrollDuration: '5m', - }); - - await expect(read()).rejects.toThrow(/shards failed/); - }); - - test('handles shards not being returned', async () => { - const index = '.myalias'; - const batch = [ - { - _id: 'such:1', - _source: { - acls: '3230a', - foos: { is: 'fun' }, - such: { num: 1 }, - type: 'such', - }, - }, - ]; - - client.search = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'x', - hits: { hits: _.cloneDeep(batch) }, - }) - ); - client.scroll = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _scroll_id: 'z', - hits: { hits: [] }, - }) - ); - - const read = Index.reader(client, index, { - batchSize: 100, - scrollDuration: '5m', - }); - - expect(await read()).toEqual(batch); - }); - }); - - describe('migrationsUpToDate', () => { - // A helper to reduce boilerplate in the hasMigration tests that follow. - async function testMigrationsUpToDate({ - index = '.myindex', - mappings, - count, - migrations, - kibanaVersion, - }: any) { - client.indices.get = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - [index]: { mappings }, - }) - ); - client.count = jest.fn().mockReturnValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - count, - _shards: { success: 1, total: 1 }, - }) - ); - - const hasMigrations = await Index.migrationsUpToDate( - client, - index, - migrations, - kibanaVersion - ); - return { hasMigrations }; - } - - test('is false if the index mappings do not contain migrationVersion', async () => { - const { hasMigrations } = await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - dashboard: { type: 'text' }, - }, - }, - count: 0, - migrations: { dashy: '2.3.4' }, - kibanaVersion: '7.10.0', - }); - - expect(hasMigrations).toBeFalsy(); - expect(client.indices.get).toHaveBeenCalledWith( - { - index: '.myalias', - }, - { - ignore: [404], - } - ); - }); - - test('is true if there are no migrations defined', async () => { - const { hasMigrations } = await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - dashboard: { type: 'text' }, - }, - }, - count: 2, - migrations: {}, - kibanaVersion: '7.10.0', - }); - - expect(hasMigrations).toBeTruthy(); - expect(client.indices.get).toHaveBeenCalledTimes(1); - }); - - test('is true if there are no documents out of date', async () => { - const { hasMigrations } = await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - dashboard: { type: 'text' }, - }, - }, - count: 0, - migrations: { dashy: '23.2.5' }, - }); - - expect(hasMigrations).toBeTruthy(); - expect(client.indices.get).toHaveBeenCalledTimes(1); - expect(client.count).toHaveBeenCalledTimes(1); - }); - - test('is false if there are documents out of date', async () => { - const { hasMigrations } = await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - dashboard: { type: 'text' }, - }, - }, - count: 3, - migrations: { dashy: '23.2.5' }, - kibanaVersion: '7.10.0', - }); - - expect(hasMigrations).toBeFalsy(); - expect(client.indices.get).toHaveBeenCalledTimes(1); - expect(client.count).toHaveBeenCalledTimes(1); - }); - - test('counts docs that are out of date', async () => { - await testMigrationsUpToDate({ - index: '.myalias', - mappings: { - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - dashboard: { type: 'text' }, - }, - }, - count: 0, - migrations: { - dashy: '23.2.5', - bashy: '99.9.3', - flashy: '3.4.5', - }, - kibanaVersion: '7.10.0', - }); - - function shouldClause(type: string, version: string) { - return { - bool: { - must: [ - { exists: { field: type } }, - { - bool: { - must_not: { term: { [`migrationVersion.${type}`]: version } }, - }, - }, - ], - }, - }; - } - - expect(client.count).toHaveBeenCalledWith({ - body: { - query: { - bool: { - should: [ - shouldClause('dashy', '23.2.5'), - shouldClause('bashy', '99.9.3'), - shouldClause('flashy', '3.4.5'), - { - bool: { - must_not: { - term: { - coreMigrationVersion: '7.10.0', - }, - }, - }, - }, - ], - }, - }, - }, - index: '.myalias', - }); - }); - }); -}); diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts deleted file mode 100644 index 64df079897722..0000000000000 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ /dev/null @@ -1,425 +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. - */ - -/* - * This module contains various functions for querying and manipulating - * elasticsearch indices. - */ - -import _ from 'lodash'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { MigrationEsClient } from './migration_es_client'; -import { IndexMapping } from '../../mappings'; -import { SavedObjectsMigrationVersion } from '../../types'; -import { AliasAction, RawDoc } from './call_cluster'; -import { SavedObjectsRawDocSource } from '../../serialization'; - -const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' }; - -export interface FullIndexInfo { - aliases: { [name: string]: object }; - exists: boolean; - indexName: string; - mappings: IndexMapping; -} - -/** - * Types that are no longer registered and need to be removed - */ -export const REMOVED_TYPES: string[] = [ - 'apm-services-telemetry', - 'background-session', - 'cases-sub-case', - 'file-upload-telemetry', - // https://github.com/elastic/kibana/issues/91869 - 'fleet-agent-events', - // Was removed in 7.12 - 'ml-telemetry', - 'server', - // https://github.com/elastic/kibana/issues/95617 - 'tsvb-validation-telemetry', - // replaced by osquery-manager-usage-metric - 'osquery-usage-metric', - // Was removed in 7.16 - 'timelion-sheet', -].sort(); - -// When migrating from the outdated index we use a read query which excludes -// saved objects which are no longer used. These saved objects will still be -// kept in the outdated index for backup purposes, but won't be available in -// the upgraded index. -export const excludeUnusedTypesQuery: estypes.QueryDslQueryContainer = { - bool: { - must_not: [ - ...REMOVED_TYPES.map((typeName) => ({ - term: { - type: typeName, - }, - })), - // https://github.com/elastic/kibana/issues/96131 - { - bool: { - must: [ - { - match: { - type: 'search-session', - }, - }, - { - match: { - 'search-session.persisted': false, - }, - }, - ], - }, - }, - ], - }, -}; - -/** - * A slight enhancement to indices.get, that adds indexName, and validates that the - * index mappings are somewhat what we expect. - */ -export async function fetchInfo(client: MigrationEsClient, index: string): Promise { - const { body, statusCode } = await client.indices.get({ index }, { ignore: [404] }); - - if (statusCode === 404) { - return { - aliases: {}, - exists: false, - indexName: index, - mappings: { dynamic: 'strict', properties: {} }, - }; - } - - const [indexName, indexInfo] = Object.entries(body)[0]; - - // @ts-expect-error @elastic/elasticsearch IndexState.alias and IndexState.mappings should be required - return assertIsSupportedIndex({ ...indexInfo, exists: true, indexName }); -} - -/** - * Creates a reader function that serves up batches of documents from the index. We aren't using - * an async generator, as that feature currently breaks Kibana's tooling. - * - * @param client - The elastic search connection - * @param index - The index to be read from - * @param {opts} - * @prop batchSize - The number of documents to read at a time - * @prop scrollDuration - The scroll duration used for scrolling through the index - */ -export function reader( - client: MigrationEsClient, - index: string, - { batchSize = 10, scrollDuration = '15m' }: { batchSize: number; scrollDuration: string } -) { - const scroll = scrollDuration; - let scrollId: string | undefined; - - const nextBatch = () => - scrollId !== undefined - ? client.scroll({ - scroll, - scroll_id: scrollId, - }) - : client.search({ - body: { - size: batchSize, - query: excludeUnusedTypesQuery, - }, - index, - scroll, - }); - - const close = async () => scrollId && (await client.clearScroll({ scroll_id: scrollId })); - - return async function read() { - const result = await nextBatch(); - assertResponseIncludeAllShards(result.body); - - scrollId = result.body._scroll_id; - const docs = result.body.hits.hits; - if (!docs.length) { - await close(); - } - - return docs; - }; -} - -/** - * Writes the specified documents to the index, throws an exception - * if any of the documents fail to save. - */ -export async function write(client: MigrationEsClient, index: string, docs: RawDoc[]) { - const { body } = await client.bulk({ - body: docs.reduce((acc: object[], doc: RawDoc) => { - acc.push({ - index: { - _id: doc._id, - _index: index, - }, - }); - - acc.push(doc._source); - - return acc; - }, []), - }); - - const err = _.find(body.items, 'index.error.reason'); - - if (!err) { - return; - } - - const exception: any = new Error(err.index!.error!.reason); - exception.detail = err; - throw exception; -} - -/** - * Checks to see if the specified index is up to date. It does this by checking - * that the index has the expected mappings and by counting - * the number of documents that have a property which has migrations defined for it, - * but which has not had those migrations applied. We don't want to cache the - * results of this function (e.g. in context or somewhere), as it is important that - * it performs the check *each* time it is called, rather than memoizing itself, - * as this is used to determine if migrations are complete. - * - * @param client - The connection to ElasticSearch - * @param index - * @param migrationVersion - The latest versions of the migrations - */ -export async function migrationsUpToDate( - client: MigrationEsClient, - index: string, - migrationVersion: SavedObjectsMigrationVersion, - kibanaVersion: string, - retryCount: number = 10 -): Promise { - try { - const indexInfo = await fetchInfo(client, index); - - if (!indexInfo.mappings.properties?.migrationVersion) { - return false; - } - - // If no migrations are actually defined, we're up to date! - if (Object.keys(migrationVersion).length <= 0) { - return true; - } - - const { body } = await client.count({ - body: { - query: { - bool: { - should: [ - ...Object.entries(migrationVersion).map(([type, latestVersion]) => ({ - bool: { - must: [ - { exists: { field: type } }, - { - bool: { - must_not: { term: { [`migrationVersion.${type}`]: latestVersion } }, - }, - }, - ], - }, - })), - { - bool: { - must_not: { - term: { - coreMigrationVersion: kibanaVersion, - }, - }, - }, - }, - ], - }, - }, - }, - index, - }); - - assertResponseIncludeAllShards(body); - - return body.count === 0; - } catch (e) { - // retry for Service Unavailable - if (e.status !== 503 || retryCount === 0) { - throw e; - } - - await new Promise((r) => setTimeout(r, 1000)); - - return await migrationsUpToDate(client, index, migrationVersion, kibanaVersion, retryCount - 1); - } -} - -export async function createIndex( - client: MigrationEsClient, - index: string, - mappings?: IndexMapping -) { - await client.indices.create({ - body: { mappings, settings }, - index, - }); -} - -/** - * Converts an index to an alias. The `alias` parameter is the desired alias name which currently - * is a concrete index. This function will reindex `alias` into a new index, delete the `alias` - * index, and then create an alias `alias` that points to the new index. - * - * @param client - The ElasticSearch connection - * @param info - Information about the mappings and name of the new index - * @param alias - The name of the index being converted to an alias - */ -export async function convertToAlias( - client: MigrationEsClient, - info: FullIndexInfo, - alias: string, - batchSize: number, - script?: string -) { - await client.indices.create({ - body: { mappings: info.mappings, settings }, - index: info.indexName, - }); - - await reindex(client, alias, info.indexName, batchSize, script); - - await claimAlias(client, info.indexName, alias, [{ remove_index: { index: alias } }]); -} - -/** - * Points the specified alias to the specified index. This is an exclusive - * alias, meaning that it will only point to one index at a time, so we - * remove any other indices from the alias. - * - * @param {MigrationEsClient} client - * @param {string} index - * @param {string} alias - * @param {AliasAction[]} aliasActions - Optional actions to be added to the updateAliases call - */ -export async function claimAlias( - client: MigrationEsClient, - index: string, - alias: string, - aliasActions: AliasAction[] = [] -) { - const { body, statusCode } = await client.indices.getAlias({ name: alias }, { ignore: [404] }); - const aliasInfo = statusCode === 404 ? {} : body; - const removeActions = Object.keys(aliasInfo).map((key) => ({ remove: { index: key, alias } })); - - await client.indices.updateAliases({ - body: { - actions: aliasActions.concat(removeActions).concat({ add: { index, alias } }), - }, - }); - - await client.indices.refresh({ index }); -} - -/** - * This is a rough check to ensure that the index being migrated satisfies at least - * some rudimentary expectations. Past Kibana indices had multiple root documents, etc - * and the migration system does not (yet?) handle those indices. They need to be upgraded - * via v5 -> v6 upgrade tools first. This file contains index-agnostic logic, and this - * check is itself index-agnostic, though the error hint is a bit Kibana specific. - * - * @param {FullIndexInfo} indexInfo - */ -function assertIsSupportedIndex(indexInfo: FullIndexInfo) { - const mappings = indexInfo.mappings as any; - const isV7Index = !!mappings.properties; - - if (!isV7Index) { - throw new Error( - `Index ${indexInfo.indexName} belongs to a version of Kibana ` + - `that cannot be automatically migrated. Reset it or use the X-Pack upgrade assistant.` - ); - } - - return indexInfo; -} - -/** - * Provides protection against reading/re-indexing against an index with missing - * shards which could result in data loss. This shouldn't be common, as the Saved - * Object indices should only ever have a single shard. This is more to handle - * instances where customers manually expand the shards of an index. - */ -function assertResponseIncludeAllShards({ _shards }: { _shards: estypes.ShardStatistics }) { - if (!_.has(_shards, 'total') || !_.has(_shards, 'successful')) { - return; - } - - const failed = _shards.total - _shards.successful; - - if (failed > 0) { - throw new Error( - `Re-index failed :: ${failed} of ${_shards.total} shards failed. ` + - `Check Elasticsearch cluster health for more information.` - ); - } -} - -/** - * Reindexes from source to dest, polling for the reindex completion. - */ -async function reindex( - client: MigrationEsClient, - source: string, - dest: string, - batchSize: number, - script?: string -) { - // We poll instead of having the request wait for completion, as for large indices, - // the request times out on the Elasticsearch side of things. We have a relatively tight - // polling interval, as the request is fairly efficient, and we don't - // want to block index migrations for too long on this. - const pollInterval = 250; - const { body: reindexBody } = await client.reindex({ - body: { - dest: { index: dest }, - source: { index: source, size: batchSize }, - script: script - ? { - source: script, - lang: 'painless', - } - : undefined, - }, - refresh: true, - wait_for_completion: false, - }); - - const task = reindexBody.task; - - let completed = false; - - while (!completed) { - await new Promise((r) => setTimeout(r, pollInterval)); - - const { body } = await client.tasks.get({ - task_id: String(task), - }); - - const e = body.error; - if (e) { - throw new Error(`Re-index failed [${e.type}] ${e.reason} :: ${JSON.stringify(e)}`); - } - - completed = body.completed; - } -} diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 84733f1bca061..0d17432a3b3d0 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -7,16 +7,14 @@ */ export { DocumentMigrator } from './document_migrator'; -export { IndexMigrator } from './index_migrator'; export { buildActiveMappings } from './build_active_mappings'; export type { LogFn, SavedObjectsMigrationLogger } from './migration_logger'; -export type { MigrationResult, MigrationStatus } from './migration_coordinator'; -export { createMigrationEsClient } from './migration_es_client'; -export type { MigrationEsClient } from './migration_es_client'; -export { excludeUnusedTypesQuery, REMOVED_TYPES } from './elastic_index'; +export { excludeUnusedTypesQuery, REMOVED_TYPES } from './unused_types'; export { TransformSavedObjectDocumentError } from './transform_saved_object_document_error'; export type { DocumentsTransformFailed, DocumentsTransformSuccess, TransformErrorObjects, } from './migrate_raw_docs'; +export { disableUnknownTypeMappingFields } from './disable_unknown_type_mapping_fields'; +export type { MigrationResult, MigrationStatus } from './types'; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts deleted file mode 100644 index beb0c1d3651c6..0000000000000 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ /dev/null @@ -1,478 +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 _ from 'lodash'; -import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; -import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; -import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; -import { IndexMigrator } from './index_migrator'; -import { MigrationOpts } from './migration_context'; -import { loggingSystemMock } from '../../../logging/logging_system.mock'; - -describe('IndexMigrator', () => { - let testOpts: jest.Mocked & { - client: ReturnType; - }; - - beforeEach(() => { - testOpts = { - batchSize: 10, - client: elasticsearchClientMock.createElasticsearchClient(), - index: '.kibana', - kibanaVersion: '7.10.0', - log: loggingSystemMock.create().get(), - setStatus: jest.fn(), - mappingProperties: {}, - pollInterval: 1, - scrollDuration: '1m', - documentMigrator: { - migrationVersion: {}, - migrate: _.identity, - migrateAndConvert: _.identity, - prepareMigrations: jest.fn(), - }, - serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - }; - }); - - test('creates the index if it does not exist', async () => { - const { client } = testOpts; - - testOpts.mappingProperties = { foo: { type: 'long' } as any }; - - withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - _meta: { - migrationMappingPropertyHashes: { - foo: '18c78c995965207ed3f6e7fc5c6e55fe', - migrationVersion: '4a1746014a75ade3a714e1db5763276f', - namespace: '2f4316de49999235636386fe51dc06c1', - namespaces: '2f4316de49999235636386fe51dc06c1', - originId: '2f4316de49999235636386fe51dc06c1', - references: '7997cf5a56cc02bdc9c93361bde732b0', - coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', - type: '2f4316de49999235636386fe51dc06c1', - updated_at: '00da57df13e94e9d98437d13ace4bfe0', - }, - }, - properties: { - foo: { type: 'long' }, - migrationVersion: { dynamic: 'true', type: 'object' }, - namespace: { type: 'keyword' }, - namespaces: { type: 'keyword' }, - originId: { type: 'keyword' }, - type: { type: 'keyword' }, - updated_at: { type: 'date' }, - references: { - type: 'nested', - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - id: { type: 'keyword' }, - }, - }, - coreMigrationVersion: { type: 'keyword' }, - }, - }, - settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, - }, - index: '.kibana_1', - }); - }); - - test('returns stats about the migration', async () => { - const { client } = testOpts; - - withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); - - const result = await new IndexMigrator(testOpts).migrate(); - - expect(result).toMatchObject({ - destIndex: '.kibana_1', - sourceIndex: '.kibana', - status: 'migrated', - }); - }); - - test('fails if there are multiple root doc types', async () => { - const { client } = testOpts; - - withIndex(client, { - index: { - '.kibana_1': { - aliases: {}, - mappings: { - foo: { properties: {} }, - doc: { - properties: { - author: { type: 'text' }, - }, - }, - }, - }, - }, - }); - - await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrow( - /use the X-Pack upgrade assistant/ - ); - }); - - test('fails if root doc type is not "doc"', async () => { - const { client } = testOpts; - - withIndex(client, { - index: { - '.kibana_1': { - aliases: {}, - mappings: { - poc: { - properties: { - author: { type: 'text' }, - }, - }, - }, - }, - }, - }); - - await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrow( - /use the X-Pack upgrade assistant/ - ); - }); - - test('retains unknown core field mappings from the previous index', async () => { - const { client } = testOpts; - - testOpts.mappingProperties = { foo: { type: 'text' } as any }; - - withIndex(client, { - index: { - '.kibana_1': { - aliases: {}, - mappings: { - properties: { - unknown_core_field: { type: 'text' }, - }, - }, - }, - }, - }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - _meta: { - migrationMappingPropertyHashes: { - foo: '625b32086eb1d1203564cf85062dd22e', - migrationVersion: '4a1746014a75ade3a714e1db5763276f', - namespace: '2f4316de49999235636386fe51dc06c1', - namespaces: '2f4316de49999235636386fe51dc06c1', - originId: '2f4316de49999235636386fe51dc06c1', - references: '7997cf5a56cc02bdc9c93361bde732b0', - coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', - type: '2f4316de49999235636386fe51dc06c1', - updated_at: '00da57df13e94e9d98437d13ace4bfe0', - }, - }, - properties: { - unknown_core_field: { type: 'text' }, - foo: { type: 'text' }, - migrationVersion: { dynamic: 'true', type: 'object' }, - namespace: { type: 'keyword' }, - namespaces: { type: 'keyword' }, - originId: { type: 'keyword' }, - type: { type: 'keyword' }, - updated_at: { type: 'date' }, - references: { - type: 'nested', - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - id: { type: 'keyword' }, - }, - }, - coreMigrationVersion: { type: 'keyword' }, - }, - }, - settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, - }, - index: '.kibana_2', - }); - }); - - test('disables complex field mappings from unknown types in the previous index', async () => { - const { client } = testOpts; - - testOpts.mappingProperties = { foo: { type: 'text' } as any }; - - withIndex(client, { - index: { - '.kibana_1': { - aliases: {}, - mappings: { - properties: { - unknown_complex_field: { properties: { description: { type: 'text' } } }, - }, - }, - }, - }, - }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith({ - body: { - mappings: { - dynamic: 'strict', - _meta: { - migrationMappingPropertyHashes: { - foo: '625b32086eb1d1203564cf85062dd22e', - migrationVersion: '4a1746014a75ade3a714e1db5763276f', - namespace: '2f4316de49999235636386fe51dc06c1', - namespaces: '2f4316de49999235636386fe51dc06c1', - originId: '2f4316de49999235636386fe51dc06c1', - references: '7997cf5a56cc02bdc9c93361bde732b0', - coreMigrationVersion: '2f4316de49999235636386fe51dc06c1', - type: '2f4316de49999235636386fe51dc06c1', - updated_at: '00da57df13e94e9d98437d13ace4bfe0', - }, - }, - properties: { - unknown_complex_field: { dynamic: false, properties: {} }, - foo: { type: 'text' }, - migrationVersion: { dynamic: 'true', type: 'object' }, - namespace: { type: 'keyword' }, - namespaces: { type: 'keyword' }, - originId: { type: 'keyword' }, - type: { type: 'keyword' }, - updated_at: { type: 'date' }, - references: { - type: 'nested', - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - id: { type: 'keyword' }, - }, - }, - coreMigrationVersion: { type: 'keyword' }, - }, - }, - settings: { number_of_shards: 1, auto_expand_replicas: '0-1' }, - }, - index: '.kibana_2', - }); - }); - - test('points the alias at the dest index', async () => { - const { client } = testOpts; - - withIndex(client, { index: { statusCode: 404 }, alias: { statusCode: 404 } }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith(expect.any(Object)); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { actions: [{ add: { alias: '.kibana', index: '.kibana_1' } }] }, - }); - }); - - test('removes previous indices from the alias', async () => { - const { client } = testOpts; - - testOpts.documentMigrator.migrationVersion = { - dashboard: '2.4.5', - }; - - withIndex(client, { numOutOfDate: 1 }); - - await new IndexMigrator(testOpts).migrate(); - - expect(client.indices.create).toHaveBeenCalledWith(expect.any(Object)); - expect(client.indices.updateAliases).toHaveBeenCalledWith({ - body: { - actions: [ - { remove: { alias: '.kibana', index: '.kibana_1' } }, - { add: { alias: '.kibana', index: '.kibana_2' } }, - ], - }, - }); - }); - - test('transforms all docs from the original index', async () => { - let count = 0; - const { client } = testOpts; - const migrateAndConvertDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { - return [{ ...doc, attributes: { name: ++count } }]; - }); - - testOpts.documentMigrator = { - migrationVersion: { foo: '1.2.3' }, - migrate: jest.fn(), - migrateAndConvert: migrateAndConvertDoc, - prepareMigrations: jest.fn(), - }; - - withIndex(client, { - numOutOfDate: 1, - docs: [ - [{ _id: 'foo:1', _source: { type: 'foo', foo: { name: 'Bar' } } }], - [{ _id: 'foo:2', _source: { type: 'foo', foo: { name: 'Baz' } } }], - ], - }); - - await new IndexMigrator(testOpts).migrate(); - - expect(count).toEqual(2); - expect(migrateAndConvertDoc).toHaveBeenNthCalledWith(1, { - id: '1', - type: 'foo', - attributes: { name: 'Bar' }, - migrationVersion: {}, - references: [], - }); - expect(migrateAndConvertDoc).toHaveBeenNthCalledWith(2, { - id: '2', - type: 'foo', - attributes: { name: 'Baz' }, - migrationVersion: {}, - references: [], - }); - - expect(client.bulk).toHaveBeenCalledTimes(2); - expect(client.bulk).toHaveBeenNthCalledWith(1, { - body: [ - { index: { _id: 'foo:1', _index: '.kibana_2' } }, - { foo: { name: 1 }, type: 'foo', migrationVersion: {}, references: [] }, - ], - }); - expect(client.bulk).toHaveBeenNthCalledWith(2, { - body: [ - { index: { _id: 'foo:2', _index: '.kibana_2' } }, - { foo: { name: 2 }, type: 'foo', migrationVersion: {}, references: [] }, - ], - }); - }); - - test('rejects when the migration function throws an error', async () => { - const { client } = testOpts; - const migrateAndConvertDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { - throw new Error('error migrating document'); - }); - - testOpts.documentMigrator = { - migrationVersion: { foo: '1.2.3' }, - migrate: jest.fn(), - migrateAndConvert: migrateAndConvertDoc, - prepareMigrations: jest.fn(), - }; - - withIndex(client, { - numOutOfDate: 1, - docs: [ - [{ _id: 'foo:1', _source: { type: 'foo', foo: { name: 'Bar' } } }], - [{ _id: 'foo:2', _source: { type: 'foo', foo: { name: 'Baz' } } }], - ], - }); - - await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrowErrorMatchingInlineSnapshot( - `"error migrating document"` - ); - }); -}); - -function withIndex( - client: ReturnType, - opts: any = {} -) { - const defaultIndex = { - '.kibana_1': { - aliases: { '.kibana': {} }, - mappings: { - dynamic: 'strict', - properties: { - migrationVersion: { dynamic: 'true', type: 'object' }, - }, - }, - }, - }; - const defaultAlias = { - '.kibana_1': {}, - }; - const { numOutOfDate = 0 } = opts; - const { alias = defaultAlias } = opts; - const { index = defaultIndex } = opts; - const { docs = [] } = opts; - const searchResult = (i: number) => ({ - _scroll_id: i, - _shards: { - successful: 1, - total: 1, - }, - hits: { - hits: docs[i] || [], - }, - }); - - let scrollCallCounter = 1; - - client.indices.get.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(index, { - statusCode: index.statusCode, - }) - ); - client.indices.getAlias.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(alias, { - statusCode: index.statusCode, - }) - ); - client.reindex.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - task: 'zeid', - _shards: { successful: 1, total: 1 }, - } as estypes.ReindexResponse) - ); - client.tasks.get.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - completed: true, - } as estypes.TasksGetResponse) - ); - client.search.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult(0) as any) - ); - client.bulk.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - items: [] as any[], - } as estypes.BulkResponse) - ); - client.count.mockReturnValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - count: numOutOfDate, - _shards: { successful: 1, total: 1 }, - } as estypes.CountResponse) - ); - // @ts-expect-error - client.scroll.mockImplementation(() => { - if (scrollCallCounter <= docs.length) { - const result = searchResult(scrollCallCounter); - scrollCallCounter++; - return elasticsearchClientMock.createSuccessTransportRequestPromise(result); - } - return elasticsearchClientMock.createSuccessTransportRequestPromise({}); - }); -} diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts deleted file mode 100644 index 0ec6fe89de1f1..0000000000000 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ /dev/null @@ -1,194 +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 { diffMappings } from './build_active_mappings'; -import * as Index from './elastic_index'; -import { migrateRawDocs } from './migrate_raw_docs'; -import { Context, migrationContext, MigrationOpts } from './migration_context'; -import { coordinateMigration, MigrationResult } from './migration_coordinator'; - -/* - * Core logic for migrating the mappings and documents in an index. - */ -export class IndexMigrator { - private opts: MigrationOpts; - - /** - * Creates an instance of IndexMigrator. - * - * @param {MigrationOpts} opts - */ - constructor(opts: MigrationOpts) { - this.opts = opts; - } - - /** - * Migrates the index, or, if another Kibana instance appears to be running the migration, - * waits for the migration to complete. - * - * @returns {Promise} - */ - public async migrate(): Promise { - const context = await migrationContext(this.opts); - - return coordinateMigration({ - log: context.log, - - pollInterval: context.pollInterval, - - setStatus: context.setStatus, - - async isMigrated() { - return !(await requiresMigration(context)); - }, - - async runMigration() { - if (await requiresMigration(context)) { - return migrateIndex(context); - } - - return { status: 'skipped' }; - }, - }); - } -} - -/** - * Determines what action the migration system needs to take (none, patch, migrate). - */ -async function requiresMigration(context: Context): Promise { - const { client, alias, documentMigrator, dest, kibanaVersion, log } = context; - - // Have all of our known migrations been run against the index? - const hasMigrations = await Index.migrationsUpToDate( - client, - alias, - documentMigrator.migrationVersion, - kibanaVersion - ); - - if (!hasMigrations) { - return true; - } - - // Is our index aliased? - const refreshedSource = await Index.fetchInfo(client, alias); - - if (!refreshedSource.aliases[alias]) { - return true; - } - - // Do the actual index mappings match our expectations? - const diffResult = diffMappings(refreshedSource.mappings, dest.mappings); - - if (diffResult) { - log.info(`Detected mapping change in "${diffResult.changedProp}"`); - - return true; - } - - return false; -} - -/** - * Performs an index migration if the source index exists, otherwise - * this simply creates the dest index with the proper mappings. - */ -async function migrateIndex(context: Context): Promise { - const startTime = Date.now(); - const { client, alias, source, dest, log } = context; - - await deleteIndexTemplates(context); - - log.info(`Creating index ${dest.indexName}.`); - - await Index.createIndex(client, dest.indexName, dest.mappings); - - await migrateSourceToDest(context); - - log.info(`Pointing alias ${alias} to ${dest.indexName}.`); - - await Index.claimAlias(client, dest.indexName, alias); - - const result: MigrationResult = { - status: 'migrated', - destIndex: dest.indexName, - sourceIndex: source.indexName, - elapsedMs: Date.now() - startTime, - }; - - log.info(`Finished in ${result.elapsedMs}ms.`); - - return result; -} - -/** - * If the obsoleteIndexTemplatePattern option is specified, this will delete any index templates - * that match it. - */ -async function deleteIndexTemplates({ client, log, obsoleteIndexTemplatePattern }: Context) { - if (!obsoleteIndexTemplatePattern) { - return; - } - - const { body: templates } = await client.cat.templates({ - format: 'json', - name: obsoleteIndexTemplatePattern, - }); - - if (!templates.length) { - return; - } - - const templateNames = templates.map((t) => t.name); - - log.info(`Removing index templates: ${templateNames}`); - - return Promise.all(templateNames.map((name) => client.indices.deleteTemplate({ name: name! }))); -} - -/** - * Moves all docs from sourceIndex to destIndex, migrating each as necessary. - * This moves documents from the concrete index, rather than the alias, to prevent - * a situation where the alias moves out from under us as we're migrating docs. - */ -async function migrateSourceToDest(context: Context) { - const { client, alias, dest, source, batchSize } = context; - const { scrollDuration, documentMigrator, log, serializer } = context; - - if (!source.exists) { - return; - } - - if (!source.aliases[alias]) { - log.info(`Reindexing ${alias} to ${source.indexName}`); - - await Index.convertToAlias(client, source, alias, batchSize, context.convertToAliasScript); - } - - const read = Index.reader(client, source.indexName, { batchSize, scrollDuration }); - - log.info(`Migrating ${source.indexName} saved objects to ${dest.indexName}`); - - while (true) { - const docs = await read(); - - if (!docs || !docs.length) { - return; - } - - log.debug(`Migrating saved objects ${docs.map((d) => d._id).join(', ')}`); - - await Index.write( - client, - dest.indexName, - // @ts-expect-error @elastic/elasticsearch _source is optional - await migrateRawDocs(serializer, documentMigrator.migrateAndConvert, docs) - ); - } -} diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts deleted file mode 100644 index 96c47bcf38d0a..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ /dev/null @@ -1,188 +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. - */ - -/** - * The MigrationOpts interface defines the minimum set of data required - * in order to properly migrate an index. MigrationContext expands this - * with computed values and values from the index being migrated, and is - * serves as a central blueprint for what migrations will end up doing. - */ - -import { Logger } from '../../../logging'; -import { MigrationEsClient } from './migration_es_client'; -import { SavedObjectsSerializer } from '../../serialization'; -import { - SavedObjectsTypeMappingDefinitions, - SavedObjectsMappingProperties, - IndexMapping, -} from '../../mappings'; -import { buildActiveMappings } from './build_active_mappings'; -import { VersionedTransformer } from './document_migrator'; -import * as Index from './elastic_index'; -import { SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; -import { KibanaMigratorStatus } from '../kibana'; - -export interface MigrationOpts { - batchSize: number; - pollInterval: number; - scrollDuration: string; - client: MigrationEsClient; - index: string; - kibanaVersion: string; - log: Logger; - setStatus: (status: KibanaMigratorStatus) => void; - mappingProperties: SavedObjectsTypeMappingDefinitions; - documentMigrator: VersionedTransformer; - serializer: SavedObjectsSerializer; - convertToAliasScript?: string; - - /** - * If specified, templates matching the specified pattern will be removed - * prior to running migrations. For example: 'kibana_index_template*' - */ - obsoleteIndexTemplatePattern?: string; -} - -/** - * @internal - */ -export interface Context { - client: MigrationEsClient; - alias: string; - source: Index.FullIndexInfo; - dest: Index.FullIndexInfo; - documentMigrator: VersionedTransformer; - kibanaVersion: string; - log: SavedObjectsMigrationLogger; - setStatus: (status: KibanaMigratorStatus) => void; - batchSize: number; - pollInterval: number; - scrollDuration: string; - serializer: SavedObjectsSerializer; - obsoleteIndexTemplatePattern?: string; - convertToAliasScript?: string; -} - -/** - * Builds up an uber object which has all of the config options, settings, - * and various info needed to migrate the source index. - */ -export async function migrationContext(opts: MigrationOpts): Promise { - const { log, client, setStatus } = opts; - const alias = opts.index; - const source = createSourceContext(await Index.fetchInfo(client, alias), alias); - const dest = createDestContext(source, alias, opts.mappingProperties); - - return { - client, - alias, - source, - dest, - kibanaVersion: opts.kibanaVersion, - log: new MigrationLogger(log), - setStatus, - batchSize: opts.batchSize, - documentMigrator: opts.documentMigrator, - pollInterval: opts.pollInterval, - scrollDuration: opts.scrollDuration, - serializer: opts.serializer, - obsoleteIndexTemplatePattern: opts.obsoleteIndexTemplatePattern, - convertToAliasScript: opts.convertToAliasScript, - }; -} - -function createSourceContext(source: Index.FullIndexInfo, alias: string) { - if (source.exists && source.indexName === alias) { - return { - ...source, - indexName: nextIndexName(alias, alias), - }; - } - - return source; -} - -function createDestContext( - source: Index.FullIndexInfo, - alias: string, - typeMappingDefinitions: SavedObjectsTypeMappingDefinitions -): Index.FullIndexInfo { - const targetMappings = disableUnknownTypeMappingFields( - buildActiveMappings(typeMappingDefinitions), - source.mappings - ); - - return { - aliases: {}, - exists: false, - indexName: nextIndexName(source.indexName, alias), - mappings: targetMappings, - }; -} - -/** - * Merges the active mappings and the source mappings while disabling the - * fields of any unknown Saved Object types present in the source index's - * mappings. - * - * Since the Saved Objects index has `dynamic: strict` defined at the - * top-level, only Saved Object types for which a mapping exists can be - * inserted into the index. To ensure that we can continue to store Saved - * Object documents belonging to a disabled plugin we define a mapping for all - * the unknown Saved Object types that were present in the source index's - * mappings. To limit the field count as much as possible, these unkwnown - * type's mappings are set to `dynamic: false`. - * - * (Since we're using the source index mappings instead of looking at actual - * document types in the inedx, we potentially add more "unknown types" than - * what would be necessary to support migrating all the data over to the - * target index.) - * - * @param activeMappings The mappings compiled from all the Saved Object types - * known to this Kibana node. - * @param sourceMappings The mappings of index used as the migration source. - * @returns The mappings that should be applied to the target index. - */ -export function disableUnknownTypeMappingFields( - activeMappings: IndexMapping, - sourceMappings: IndexMapping -): IndexMapping { - const targetTypes = Object.keys(activeMappings.properties); - - const disabledTypesProperties = Object.keys(sourceMappings.properties ?? {}) - .filter((sourceType) => { - const isObjectType = 'properties' in sourceMappings.properties[sourceType]; - // Only Object/Nested datatypes can be excluded from the field count by - // using `dynamic: false`. - return !targetTypes.includes(sourceType) && isObjectType; - }) - .reduce((disabledTypesAcc, sourceType) => { - disabledTypesAcc[sourceType] = { dynamic: false, properties: {} }; - return disabledTypesAcc; - }, {} as SavedObjectsMappingProperties); - - return { - ...activeMappings, - properties: { - ...sourceMappings.properties, - ...disabledTypesProperties, - ...activeMappings.properties, - }, - }; -} - -/** - * Gets the next index name in a sequence, based on specified current index's info. - * We're using a numeric counter to create new indices. So, `.kibana_1`, `.kibana_2`, etc - * There are downsides to this, but it seemed like a simple enough approach. - */ -function nextIndexName(indexName: string, alias: string) { - const indexSuffix = (indexName.match(/[\d]+$/) || [])[0]; - const indexNum = parseInt(indexSuffix, 10) || 0; - return `${alias}_${indexNum + 1}`; -} diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts deleted file mode 100644 index 63476a15d77cd..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts +++ /dev/null @@ -1,75 +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 { coordinateMigration } from './migration_coordinator'; -import { createSavedObjectsMigrationLoggerMock } from '../mocks'; - -describe('coordinateMigration', () => { - const log = createSavedObjectsMigrationLoggerMock(); - - test('waits for isMigrated, if there is an index conflict', async () => { - const pollInterval = 1; - const runMigration = jest.fn(() => { - // eslint-disable-next-line no-throw-literal - throw { body: { error: { index: '.foo', type: 'resource_already_exists_exception' } } }; - }); - const isMigrated = jest.fn(); - const setStatus = jest.fn(); - - isMigrated.mockResolvedValueOnce(false).mockResolvedValueOnce(true); - - await coordinateMigration({ - log, - runMigration, - pollInterval, - isMigrated, - setStatus, - }); - - expect(runMigration).toHaveBeenCalledTimes(1); - expect(isMigrated).toHaveBeenCalledTimes(2); - const warnings = log.warning.mock.calls.filter((msg: any) => /deleting index \.foo/.test(msg)); - expect(warnings.length).toEqual(1); - }); - - test('does not poll if the runMigration succeeds', async () => { - const pollInterval = 1; - const runMigration = jest.fn(() => Promise.resolve()); - const isMigrated = jest.fn(() => Promise.resolve(true)); - const setStatus = jest.fn(); - - await coordinateMigration({ - log, - runMigration, - pollInterval, - isMigrated, - setStatus, - }); - expect(isMigrated).not.toHaveBeenCalled(); - }); - - test('does not swallow exceptions', async () => { - const pollInterval = 1; - const runMigration = jest.fn(() => { - throw new Error('Doh'); - }); - const isMigrated = jest.fn(() => Promise.resolve(true)); - const setStatus = jest.fn(); - - await expect( - coordinateMigration({ - log, - runMigration, - pollInterval, - isMigrated, - setStatus, - }) - ).rejects.toThrow(/Doh/); - expect(isMigrated).not.toHaveBeenCalled(); - }); -}); diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts deleted file mode 100644 index 5b99f050b0ece..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.ts +++ /dev/null @@ -1,124 +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. - */ - -/* - * This provides a mechanism for preventing multiple Kibana instances from - * simultaneously running migrations on the same index. It synchronizes this - * by handling index creation conflicts, and putting this instance into a - * poll loop that periodically checks to see if the index is migrated. - * - * The reason we have to coordinate this, rather than letting each Kibana instance - * perform duplicate work, is that if we allowed each Kibana to simply run migrations in - * parallel, they would each try to reindex and each try to create the destination index. - * If those indices already exist, it may be due to contention between multiple Kibana - * instances (which is safe to ignore), but it may be due to a partially completed migration, - * or someone tampering with the Kibana alias. In these cases, it's not clear that we should - * just migrate data into an existing index. Such an action could result in data loss. Instead, - * we should probably fail, and the Kibana sys-admin should clean things up before relaunching - * Kibana. - */ - -import _ from 'lodash'; -import { KibanaMigratorStatus } from '../kibana'; -import { SavedObjectsMigrationLogger } from './migration_logger'; - -const DEFAULT_POLL_INTERVAL = 15000; - -export type MigrationStatus = - | 'waiting_to_start' - | 'waiting_for_other_nodes' - | 'running' - | 'completed'; - -export type MigrationResult = - | { status: 'skipped' } - | { status: 'patched' } - | { - status: 'migrated'; - destIndex: string; - sourceIndex: string; - elapsedMs: number; - }; - -interface Opts { - runMigration: () => Promise; - isMigrated: () => Promise; - setStatus: (status: KibanaMigratorStatus) => void; - log: SavedObjectsMigrationLogger; - pollInterval?: number; -} - -/** - * Runs the migration specified by opts. If the migration fails due to an index - * creation conflict, this falls into a polling loop, checking every pollInterval - * milliseconds if the index is migrated. - * - * @export - * @param {Opts} opts - * @prop {Migration} runMigration - A function that runs the index migration - * @prop {IsMigrated} isMigrated - A function which checks if the index is already migrated - * @prop {Logger} log - The migration logger - * @prop {number} pollInterval - How often, in ms, to check that the index is migrated - * @returns - */ -export async function coordinateMigration(opts: Opts): Promise { - try { - return await opts.runMigration(); - } catch (error) { - const waitingIndex = handleIndexExists(error, opts.log); - if (waitingIndex) { - opts.setStatus({ status: 'waiting_for_other_nodes', waitingIndex }); - await waitForMigration(opts.isMigrated, opts.pollInterval); - return { status: 'skipped' }; - } - throw error; - } -} - -/** - * If the specified error is an index exists error, this logs a warning, - * and is the cue for us to fall into a polling loop, waiting for some - * other Kibana instance to complete the migration. - */ -function handleIndexExists(error: any, log: SavedObjectsMigrationLogger): string | undefined { - const isIndexExistsError = - _.get(error, 'body.error.type') === 'resource_already_exists_exception'; - if (!isIndexExistsError) { - return undefined; - } - - const index = _.get(error, 'body.error.index'); - - log.warning( - `Another Kibana instance appears to be migrating the index. Waiting for ` + - `that migration to complete. If no other Kibana instance is attempting ` + - `migrations, you can get past this message by deleting index ${index} and ` + - `restarting Kibana.` - ); - - return index; -} - -/** - * Polls isMigrated every pollInterval milliseconds until it returns true. - */ -async function waitForMigration( - isMigrated: () => Promise, - pollInterval = DEFAULT_POLL_INTERVAL -) { - while (true) { - if (await isMigrated()) { - return; - } - await sleep(pollInterval); - } -} - -function sleep(ms: number) { - return new Promise((r) => setTimeout(r, ms)); -} diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts deleted file mode 100644 index 593973ad2e9ba..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.test.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 migrationRetryCallClusterMock = jest.fn((fn) => fn()); -jest.doMock('../../../elasticsearch/client/retry_call_cluster', () => ({ - migrationRetryCallCluster: migrationRetryCallClusterMock, -})); diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts deleted file mode 100644 index 75dbdf55e55fc..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.test.ts +++ /dev/null @@ -1,55 +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 { migrationRetryCallClusterMock } from './migration_es_client.test.mock'; - -import { createMigrationEsClient, MigrationEsClient } from './migration_es_client'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; -import { loggerMock } from '../../../logging/logger.mock'; -import { SavedObjectsErrorHelpers } from '../../service/lib/errors'; - -describe('MigrationEsClient', () => { - let client: ReturnType; - let migrationEsClient: MigrationEsClient; - - beforeEach(() => { - client = elasticsearchClientMock.createElasticsearchClient(); - migrationEsClient = createMigrationEsClient(client, loggerMock.create()); - migrationRetryCallClusterMock.mockClear(); - }); - - it('delegates call to ES client method', async () => { - expect(migrationEsClient.bulk).toStrictEqual(expect.any(Function)); - await migrationEsClient.bulk({ body: [] }); - expect(client.bulk).toHaveBeenCalledTimes(1); - }); - - it('wraps a method call in migrationRetryCallClusterMock', async () => { - await migrationEsClient.bulk({ body: [] }); - expect(migrationRetryCallClusterMock).toHaveBeenCalledTimes(1); - }); - - it('sets maxRetries: 0 to delegate retry logic to migrationRetryCallCluster', async () => { - expect(migrationEsClient.bulk).toStrictEqual(expect.any(Function)); - await migrationEsClient.bulk({ body: [] }); - expect(client.bulk).toHaveBeenCalledWith( - expect.any(Object), - expect.objectContaining({ maxRetries: 0 }) - ); - }); - - it('do not transform elasticsearch errors into saved objects errors', async () => { - expect.assertions(1); - client.bulk = jest.fn().mockRejectedValue(new Error('reason')); - try { - await migrationEsClient.bulk({ body: [] }); - } catch (e) { - expect(SavedObjectsErrorHelpers.isSavedObjectsClientError(e)).toBe(false); - } - }); -}); diff --git a/src/core/server/saved_objects/migrations/core/migration_es_client.ts b/src/core/server/saved_objects/migrations/core/migration_es_client.ts deleted file mode 100644 index 243b724eb2a67..0000000000000 --- a/src/core/server/saved_objects/migrations/core/migration_es_client.ts +++ /dev/null @@ -1,78 +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 type { Client, TransportRequestOptions } from '@elastic/elasticsearch'; -import { get } from 'lodash'; -import { set } from '@elastic/safer-lodash-set'; - -import { ElasticsearchClient } from '../../../elasticsearch'; -import { migrationRetryCallCluster } from '../../../elasticsearch/client/retry_call_cluster'; -import { Logger } from '../../../logging'; - -const methods = [ - 'bulk', - 'cat.templates', - 'clearScroll', - 'count', - 'indices.create', - 'indices.deleteTemplate', - 'indices.get', - 'indices.getAlias', - 'indices.refresh', - 'indices.updateAliases', - 'reindex', - 'search', - 'scroll', - 'tasks.get', -] as const; - -type MethodName = typeof methods[number]; - -export interface MigrationEsClient { - bulk: ElasticsearchClient['bulk']; - cat: { - templates: ElasticsearchClient['cat']['templates']; - }; - clearScroll: ElasticsearchClient['clearScroll']; - count: ElasticsearchClient['count']; - indices: { - create: ElasticsearchClient['indices']['create']; - delete: ElasticsearchClient['indices']['delete']; - deleteTemplate: ElasticsearchClient['indices']['deleteTemplate']; - get: ElasticsearchClient['indices']['get']; - getAlias: ElasticsearchClient['indices']['getAlias']; - refresh: ElasticsearchClient['indices']['refresh']; - updateAliases: ElasticsearchClient['indices']['updateAliases']; - }; - reindex: ElasticsearchClient['reindex']; - search: ElasticsearchClient['search']; - scroll: ElasticsearchClient['scroll']; - tasks: { - get: ElasticsearchClient['tasks']['get']; - }; -} - -export function createMigrationEsClient( - client: ElasticsearchClient | Client, - log: Logger, - delay?: number -): MigrationEsClient { - return methods.reduce((acc: MigrationEsClient, key: MethodName) => { - set(acc, key, async (params?: unknown, options?: TransportRequestOptions) => { - const fn = get(client, key); - if (!fn) { - throw new Error(`unknown ElasticsearchClient client method [${key}]`); - } - return await migrationRetryCallCluster( - () => fn.call(client, params, { maxRetries: 0, meta: true, ...options }), - log, - delay - ); - }); - return acc; - }, {} as MigrationEsClient); -} diff --git a/src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts b/src/core/server/saved_objects/migrations/core/types.ts similarity index 53% rename from src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts rename to src/core/server/saved_objects/migrations/core/types.ts index 35dc08d50072d..61985d8f10996 100644 --- a/src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/types.ts @@ -6,10 +6,18 @@ * Side Public License, v 1. */ -import { mockKibanaMigrator } from '../kibana_migrator.mock'; +export type MigrationStatus = + | 'waiting_to_start' + | 'waiting_for_other_nodes' + | 'running' + | 'completed'; -export const mockKibanaMigratorInstance = mockKibanaMigrator.create(); - -const mockConstructor = jest.fn().mockImplementation(() => mockKibanaMigratorInstance); - -export const KibanaMigrator = mockConstructor; +export type MigrationResult = + | { status: 'skipped' } + | { status: 'patched' } + | { + status: 'migrated'; + destIndex: string; + sourceIndex: string; + elapsedMs: number; + }; diff --git a/src/core/server/saved_objects/migrations/core/unused_types.ts b/src/core/server/saved_objects/migrations/core/unused_types.ts new file mode 100644 index 0000000000000..f5f6647201bbf --- /dev/null +++ b/src/core/server/saved_objects/migrations/core/unused_types.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +/** + * Types that are no longer registered and need to be removed + */ +export const REMOVED_TYPES: string[] = [ + 'apm-services-telemetry', + 'background-session', + 'cases-sub-case', + 'file-upload-telemetry', + // https://github.com/elastic/kibana/issues/91869 + 'fleet-agent-events', + // Was removed in 7.12 + 'ml-telemetry', + 'server', + // https://github.com/elastic/kibana/issues/95617 + 'tsvb-validation-telemetry', + // replaced by osquery-manager-usage-metric + 'osquery-usage-metric', + // Was removed in 7.16 + 'timelion-sheet', +].sort(); + +// When migrating from the outdated index we use a read query which excludes +// saved objects which are no longer used. These saved objects will still be +// kept in the outdated index for backup purposes, but won't be available in +// the upgraded index. +export const excludeUnusedTypesQuery: estypes.QueryDslQueryContainer = { + bool: { + must_not: [ + ...REMOVED_TYPES.map((typeName) => ({ + term: { + type: typeName, + }, + })), + // https://github.com/elastic/kibana/issues/96131 + { + bool: { + must: [ + { + match: { + type: 'search-session', + }, + }, + { + match: { + 'search-session.persisted': false, + }, + }, + ], + }, + }, + ], + }, +}; diff --git a/src/core/server/saved_objects/migrations/index.ts b/src/core/server/saved_objects/migrations/index.ts index 20b86ee6d3739..91be12425c605 100644 --- a/src/core/server/saved_objects/migrations/index.ts +++ b/src/core/server/saved_objects/migrations/index.ts @@ -7,8 +7,8 @@ */ export type { MigrationResult } from './core'; -export { KibanaMigrator } from './kibana'; -export type { IKibanaMigrator } from './kibana'; +export { KibanaMigrator } from './kibana_migrator'; +export type { IKibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; export type { SavedObjectMigrationFn, SavedObjectMigrationMap, diff --git a/src/core/server/saved_objects/migrationsv2/initial_state.test.ts b/src/core/server/saved_objects/migrations/initial_state.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/initial_state.test.ts rename to src/core/server/saved_objects/migrations/initial_state.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/initial_state.ts b/src/core/server/saved_objects/migrations/initial_state.ts similarity index 98% rename from src/core/server/saved_objects/migrationsv2/initial_state.ts rename to src/core/server/saved_objects/migrations/initial_state.ts index a61967be9242c..f074f123c8930 100644 --- a/src/core/server/saved_objects/migrationsv2/initial_state.ts +++ b/src/core/server/saved_objects/migrations/initial_state.ts @@ -11,7 +11,7 @@ import { IndexMapping } from '../mappings'; import { SavedObjectsMigrationVersion } from '../../../types'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import type { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; -import { InitState } from './types'; +import { InitState } from './state'; import { excludeUnusedTypesQuery } from '../migrations/core'; /** diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/.gitignore b/src/core/server/saved_objects/migrations/integration_tests/.gitignore similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/.gitignore rename to src/core/server/saved_objects/migrations/integration_tests/.gitignore diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/7.7.2_xpack_100k.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_failed_action_tasks.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_failed_action_tasks.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_failed_action_tasks.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/7_13_0_failed_action_tasks.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_transform_failures.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_transform_failures.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_transform_failures.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/7_13_0_transform_failures.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_unknown_types.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_unknown_types.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/7_13_0_unknown_types.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_5k_so_node_01.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_5k_so_node_01.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_5k_so_node_01.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_5k_so_node_01.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_5k_so_node_02.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_5k_so_node_02.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_5k_so_node_02.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_5k_so_node_02.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_concurrent_5k_foo.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_concurrent_5k_foo.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_concurrent_5k_foo.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_concurrent_5k_foo.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_with_corrupted_so.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_with_corrupted_so.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_unknown_so.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_with_unknown_so.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_unknown_so.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.0_with_unknown_so.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.13_1.5k_failed_action_tasks.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.3.0_xpack_sample_saved_objects.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.7.2_xpack_100k_obj.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7.7.2_xpack_100k_obj.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.7.2_xpack_100k_obj.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7.7.2_xpack_100k_obj.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/7_13_corrupt_and_transform_failures_docs.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_document_migration_failure.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_document_migration_failure.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_document_migration_failure.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_document_migration_failure.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip b/src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip rename to src/core/server/saved_objects/migrations/integration_tests/archives/8.0.0_v1_migrations_sample_data_saved_objects.zip diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrations/integration_tests/cleanup.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/cleanup.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/collects_corrupt_docs.test.ts b/src/core/server/saved_objects/migrations/integration_tests/collects_corrupt_docs.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/collects_corrupt_docs.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/collects_corrupt_docs.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts b/src/core/server/saved_objects/migrations/integration_tests/corrupt_outdated_docs.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/corrupt_outdated_docs.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts b/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_older_v1.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_same_v1.test.ts b/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_same_v1.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_es_nodes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/multiple_es_nodes.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/multiple_es_nodes.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/multiple_es_nodes.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/multiple_kibana_nodes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/multiple_kibana_nodes.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/multiple_kibana_nodes.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/multiple_kibana_nodes.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts b/src/core/server/saved_objects/migrations/integration_tests/outdated_docs.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/outdated_docs.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrations/integration_tests/rewriting_id.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/rewriting_id.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts rename to src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts diff --git a/src/core/server/saved_objects/migrations/kibana/index.ts b/src/core/server/saved_objects/migrations/kibana/index.ts deleted file mode 100644 index 52755ee0aed71..0000000000000 --- a/src/core/server/saved_objects/migrations/kibana/index.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 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 { KibanaMigrator } from './kibana_migrator'; -export type { IKibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana_migrator.mock.ts similarity index 83% rename from src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts rename to src/core/server/saved_objects/migrations/kibana_migrator.mock.ts index 660300ea867ff..24486a9336122 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.mock.ts @@ -7,11 +7,11 @@ */ import { IKibanaMigrator, KibanaMigratorStatus } from './kibana_migrator'; -import { buildActiveMappings } from '../core'; +import { buildActiveMappings } from './core'; + const { mergeTypes } = jest.requireActual('./kibana_migrator'); -import { SavedObjectsType } from '../../types'; +import { SavedObjectsType } from '../types'; import { BehaviorSubject } from 'rxjs'; -import { ByteSizeValue } from '@kbn/config-schema'; const defaultSavedObjectTypes: SavedObjectsType[] = [ { @@ -36,14 +36,6 @@ const createMigrator = ( ) => { const mockMigrator: jest.Mocked = { kibanaVersion: '8.0.0-testing', - soMigrationsConfig: { - batchSize: 100, - maxBatchSizeBytes: ByteSizeValue.parse('30kb'), - scrollDuration: '15m', - pollInterval: 1500, - skip: false, - retryAttempts: 10, - }, runMigrations: jest.fn(), getActiveMappings: jest.fn(), migrateDocument: jest.fn(), diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana_migrator.test.ts similarity index 96% rename from src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts rename to src/core/server/saved_objects/migrations/kibana_migrator.test.ts index fe3d6c469726d..eb7b72f144031 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.test.ts @@ -9,19 +9,19 @@ import { take } from 'rxjs/operators'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; +import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; -import { loggingSystemMock } from '../../../logging/logging_system.mock'; -import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; -import { SavedObjectsType } from '../../types'; -import { DocumentMigrator } from '../core/document_migrator'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsType } from '../types'; +import { DocumentMigrator } from './core/document_migrator'; import { ByteSizeValue } from '@kbn/config-schema'; -jest.mock('../core/document_migrator', () => { +jest.mock('./core/document_migrator', () => { return { // Create a mock for spying on the constructor DocumentMigrator: jest.fn().mockImplementation((...args) => { - const { DocumentMigrator: RealDocMigrator } = jest.requireActual('../core/document_migrator'); + const { DocumentMigrator: RealDocMigrator } = jest.requireActual('./core/document_migrator'); return new RealDocMigrator(args[0]); }), }; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana_migrator.ts similarity index 89% rename from src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts rename to src/core/server/saved_objects/migrations/kibana_migrator.ts index 198983538c93d..fa1172c0684a7 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.ts @@ -13,22 +13,22 @@ import { BehaviorSubject } from 'rxjs'; import Semver from 'semver'; -import { ElasticsearchClient } from '../../../elasticsearch'; -import { Logger } from '../../../logging'; -import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings'; +import { ElasticsearchClient } from '../../elasticsearch'; +import { Logger } from '../../logging'; +import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../mappings'; import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer, SavedObjectsRawDoc, -} from '../../serialization'; -import { buildActiveMappings, MigrationResult, MigrationStatus } from '../core'; -import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; -import { createIndexMap } from '../core/build_index_map'; -import { SavedObjectsMigrationConfigType } from '../../saved_objects_config'; -import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; -import { SavedObjectsType } from '../../types'; -import { runResilientMigrator } from '../../migrationsv2'; -import { migrateRawDocsSafely } from '../core/migrate_raw_docs'; +} from '../serialization'; +import { buildActiveMappings, MigrationResult, MigrationStatus } from './core'; +import { DocumentMigrator, VersionedTransformer } from './core/document_migrator'; +import { createIndexMap } from './core/build_index_map'; +import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsType } from '../types'; +import { runResilientMigrator } from './run_resilient_migrator'; +import { migrateRawDocsSafely } from './core/migrate_raw_docs'; export interface KibanaMigratorOptions { client: ElasticsearchClient; @@ -37,7 +37,6 @@ export interface KibanaMigratorOptions { kibanaIndex: string; kibanaVersion: string; logger: Logger; - migrationsRetryDelay?: number; } export type IKibanaMigrator = Pick; @@ -64,10 +63,8 @@ export class KibanaMigrator { status: 'waiting_to_start', }); private readonly activeMappings: IndexMapping; - // TODO migrationsV2: make private once we remove migrations v1 + private readonly soMigrationsConfig: SavedObjectsMigrationConfigType; public readonly kibanaVersion: string; - // TODO migrationsV2: make private once we remove migrations v1 - public readonly soMigrationsConfig: SavedObjectsMigrationConfigType; /** * Creates an instance of KibanaMigrator. @@ -79,7 +76,6 @@ export class KibanaMigrator { soMigrationsConfig, kibanaVersion, logger, - migrationsRetryDelay, }: KibanaMigratorOptions) { this.client = client; this.kibanaIndex = kibanaIndex; diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts rename to src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts index c53bd7bbc53dd..3bc07c0fea0c1 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts @@ -15,7 +15,7 @@ import * as Option from 'fp-ts/lib/Option'; import { errors } from '@elastic/elasticsearch'; import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { LoggerAdapter } from '../../logging/logger_adapter'; -import { AllControlStates, State } from './types'; +import { AllControlStates, State } from './state'; import { createInitialState } from './initial_state'; import { ByteSizeValue } from '@kbn/config-schema'; diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrations/migrations_state_action_machine.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts rename to src/core/server/saved_objects/migrations/migrations_state_action_machine.ts index 3a5e592a8b9bf..87b78102371d3 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrations/migrations_state_action_machine.ts @@ -13,7 +13,7 @@ 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 { ReindexSourceToTempTransform, ReindexSourceToTempIndexBulk, State } from './types'; +import { ReindexSourceToTempTransform, ReindexSourceToTempIndexBulk, State } from './state'; import { SavedObjectsRawDoc } from '../serialization'; interface StateTransitionLogMeta extends LogMeta { diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.mocks.ts b/src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.mocks.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.mocks.ts rename to src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.mocks.ts diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts b/src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.ts similarity index 94% rename from src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts rename to src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.ts index 9c0ef0d1a2cb6..ff8ff57d41ce4 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts +++ b/src/core/server/saved_objects/migrations/migrations_state_machine_cleanup.ts @@ -8,7 +8,7 @@ import type { ElasticsearchClient } from '../../elasticsearch'; import * as Actions from './actions'; -import type { State } from './types'; +import type { State } from './state'; export async function cleanup(client: ElasticsearchClient, state?: State) { if (!state) return; diff --git a/src/core/server/saved_objects/migrationsv2/model/create_batches.test.ts b/src/core/server/saved_objects/migrations/model/create_batches.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/create_batches.test.ts rename to src/core/server/saved_objects/migrations/model/create_batches.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/create_batches.ts b/src/core/server/saved_objects/migrations/model/create_batches.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/create_batches.ts rename to src/core/server/saved_objects/migrations/model/create_batches.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts b/src/core/server/saved_objects/migrations/model/extract_errors.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts rename to src/core/server/saved_objects/migrations/model/extract_errors.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts b/src/core/server/saved_objects/migrations/model/extract_errors.ts similarity index 97% rename from src/core/server/saved_objects/migrationsv2/model/extract_errors.ts rename to src/core/server/saved_objects/migrations/model/extract_errors.ts index 082e6344afffc..3dabb09043376 100644 --- a/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts +++ b/src/core/server/saved_objects/migrations/model/extract_errors.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { TransformErrorObjects } from '../../migrations/core'; +import { TransformErrorObjects } from '../core'; import { CheckForUnknownDocsFoundDoc } from '../actions'; /** diff --git a/src/core/server/saved_objects/migrationsv2/model/helpers.ts b/src/core/server/saved_objects/migrations/model/helpers.ts similarity index 98% rename from src/core/server/saved_objects/migrationsv2/model/helpers.ts rename to src/core/server/saved_objects/migrations/model/helpers.ts index 4e920608594b1..c3a4c85679680 100644 --- a/src/core/server/saved_objects/migrationsv2/model/helpers.ts +++ b/src/core/server/saved_objects/migrations/model/helpers.ts @@ -7,7 +7,7 @@ */ import { gt, valid } from 'semver'; -import { State } from '../types'; +import { State } from '../state'; import { IndexMapping } from '../../mappings'; import { FetchIndexResponse } from '../actions'; diff --git a/src/core/server/saved_objects/migrationsv2/model/index.ts b/src/core/server/saved_objects/migrations/model/index.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/index.ts rename to src/core/server/saved_objects/migrations/model/index.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/model.test.ts b/src/core/server/saved_objects/migrations/model/model.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/model/model.test.ts rename to src/core/server/saved_objects/migrations/model/model.test.ts index e4ab5a0f11039..7cd5f63640d1d 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.test.ts +++ b/src/core/server/saved_objects/migrations/model/model.test.ts @@ -40,7 +40,7 @@ import type { ReindexSourceToTempIndexBulk, CheckUnknownDocumentsState, CalculateExcludeFiltersState, -} from '../types'; +} from '../state'; import { SavedObjectsRawDoc } from '../../serialization'; import { TransformErrorObjects, TransformSavedObjectDocumentError } from '../../migrations/core'; import { AliasAction, RetryableEsClientError } from '../actions'; diff --git a/src/core/server/saved_objects/migrationsv2/model/model.ts b/src/core/server/saved_objects/migrations/model/model.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/model/model.ts rename to src/core/server/saved_objects/migrations/model/model.ts index ff27045dd91ce..522a43a737cb7 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.ts +++ b/src/core/server/saved_objects/migrations/model/model.ts @@ -8,12 +8,13 @@ import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; - import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + import { AliasAction, isLeftTypeof } from '../actions'; -import { AllActionStates, MigrationLog, State } from '../types'; +import { MigrationLog } from '../types'; +import { AllActionStates, State } from '../state'; import type { ResponseType } from '../next'; -import { disableUnknownTypeMappingFields } from '../../migrations/core/migration_context'; +import { disableUnknownTypeMappingFields } from '../core'; import { createInitialProgress, incrementProcessedProgress, diff --git a/src/core/server/saved_objects/migrationsv2/model/progress.test.ts b/src/core/server/saved_objects/migrations/model/progress.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/progress.test.ts rename to src/core/server/saved_objects/migrations/model/progress.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/progress.ts b/src/core/server/saved_objects/migrations/model/progress.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/progress.ts rename to src/core/server/saved_objects/migrations/model/progress.ts diff --git a/src/core/server/saved_objects/migrationsv2/model/retry_state.test.ts b/src/core/server/saved_objects/migrations/model/retry_state.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/model/retry_state.test.ts rename to src/core/server/saved_objects/migrations/model/retry_state.test.ts index d49e570e0cdef..5a195f8597182 100644 --- a/src/core/server/saved_objects/migrationsv2/model/retry_state.test.ts +++ b/src/core/server/saved_objects/migrations/model/retry_state.test.ts @@ -7,7 +7,7 @@ */ import { resetRetryState, delayRetryState } from './retry_state'; -import { State } from '../types'; +import { State } from '../state'; const createState = (parts: Record) => { return parts as State; diff --git a/src/core/server/saved_objects/migrationsv2/model/retry_state.ts b/src/core/server/saved_objects/migrations/model/retry_state.ts similarity index 97% rename from src/core/server/saved_objects/migrationsv2/model/retry_state.ts rename to src/core/server/saved_objects/migrations/model/retry_state.ts index 5d69d32a7160c..02057a6af2061 100644 --- a/src/core/server/saved_objects/migrationsv2/model/retry_state.ts +++ b/src/core/server/saved_objects/migrations/model/retry_state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { State } from '../types'; +import { State } from '../state'; export const delayRetryState = ( state: S, diff --git a/src/core/server/saved_objects/migrationsv2/model/types.ts b/src/core/server/saved_objects/migrations/model/types.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/model/types.ts rename to src/core/server/saved_objects/migrations/model/types.ts diff --git a/src/core/server/saved_objects/migrationsv2/next.test.ts b/src/core/server/saved_objects/migrations/next.test.ts similarity index 96% rename from src/core/server/saved_objects/migrationsv2/next.test.ts rename to src/core/server/saved_objects/migrations/next.test.ts index a34480fc311cd..98a8690844872 100644 --- a/src/core/server/saved_objects/migrationsv2/next.test.ts +++ b/src/core/server/saved_objects/migrations/next.test.ts @@ -8,7 +8,7 @@ import { ElasticsearchClient } from '../../elasticsearch'; import { next } from './next'; -import { State } from './types'; +import { State } from './state'; describe('migrations v2 next', () => { it.todo('when state.retryDelay > 0 delays execution of the next action'); diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrations/next.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/next.ts rename to src/core/server/saved_objects/migrations/next.ts index 433c0998f7567..1368ca308110d 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrations/next.ts @@ -31,7 +31,6 @@ import type { CloneTempToSource, SetTempWriteBlock, WaitForYellowSourceState, - TransformRawDocs, TransformedDocumentsBulkIndex, ReindexSourceToTempIndexBulk, OutdatedDocumentsSearchOpenPit, @@ -41,7 +40,8 @@ import type { OutdatedDocumentsRefresh, CheckUnknownDocumentsState, CalculateExcludeFiltersState, -} from './types'; +} from './state'; +import { TransformRawDocs } from './types'; import * as Actions from './actions'; import { ElasticsearchClient } from '../../elasticsearch'; diff --git a/src/core/server/saved_objects/migrationsv2/index.ts b/src/core/server/saved_objects/migrations/run_resilient_migrator.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/index.ts rename to src/core/server/saved_objects/migrations/run_resilient_migrator.ts diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrations/state.ts similarity index 96% rename from src/core/server/saved_objects/migrationsv2/types.ts rename to src/core/server/saved_objects/migrations/state.ts index e68e04e5267cc..7073167bfbd1b 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrations/state.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import * as TaskEither from 'fp-ts/lib/TaskEither'; import * as Option from 'fp-ts/lib/Option'; import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ControlState } from './state_action_machine'; @@ -14,23 +13,8 @@ import { AliasAction } from './actions'; import { IndexMapping } from '../mappings'; import { SavedObjectsRawDoc } from '..'; import { TransformErrorObjects } from '../migrations/core'; -import { - DocumentsTransformFailed, - DocumentsTransformSuccess, -} from '../migrations/core/migrate_raw_docs'; import { SavedObjectTypeExcludeFromUpgradeFilterHook } from '../types'; - -export type MigrationLogLevel = 'error' | 'info' | 'warning'; - -export interface MigrationLog { - level: MigrationLogLevel; - message: string; -} - -export interface Progress { - processed: number | undefined; - total: number | undefined; -} +import { MigrationLog, Progress } from './types'; export interface BaseState extends ControlState { /** The first part of the index name such as `.kibana` or `.kibana_task_manager` */ @@ -462,7 +446,3 @@ export type AllControlStates = State['controlState']; * 'FATAL' and 'DONE'). */ export type AllActionStates = Exclude; - -export type TransformRawDocs = ( - rawDocs: SavedObjectsRawDoc[] -) => TaskEither.TaskEither; diff --git a/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts b/src/core/server/saved_objects/migrations/state_action_machine.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts rename to src/core/server/saved_objects/migrations/state_action_machine.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/state_action_machine.ts b/src/core/server/saved_objects/migrations/state_action_machine.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/state_action_machine.ts rename to src/core/server/saved_objects/migrations/state_action_machine.ts diff --git a/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts b/src/core/server/saved_objects/migrations/test_helpers/retry.test.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts rename to src/core/server/saved_objects/migrations/test_helpers/retry.test.ts diff --git a/src/core/server/saved_objects/migrationsv2/test_helpers/retry_async.ts b/src/core/server/saved_objects/migrations/test_helpers/retry_async.ts similarity index 100% rename from src/core/server/saved_objects/migrationsv2/test_helpers/retry_async.ts rename to src/core/server/saved_objects/migrations/test_helpers/retry_async.ts diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index fe5a79dac12c3..a52ba56bc8ff6 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -6,8 +6,11 @@ * Side Public License, v 1. */ -import { SavedObjectUnsanitizedDoc } from '../serialization'; -import { SavedObjectsMigrationLogger } from './core/migration_logger'; +import * as TaskEither from 'fp-ts/TaskEither'; +import type { SavedObjectUnsanitizedDoc } from '../serialization'; +import type { SavedObjectsMigrationLogger } from './core'; +import { SavedObjectsRawDoc } from '../serialization'; +import { DocumentsTransformFailed, DocumentsTransformSuccess } from './core'; /** * A migration function for a {@link SavedObjectsType | saved object type} @@ -91,3 +94,23 @@ export interface SavedObjectMigrationContext { export interface SavedObjectMigrationMap { [version: string]: SavedObjectMigrationFn; } + +/** @internal */ +export type TransformRawDocs = ( + rawDocs: SavedObjectsRawDoc[] +) => TaskEither.TaskEither; + +/** @internal */ +export type MigrationLogLevel = 'error' | 'info' | 'warning'; + +/** @internal */ +export interface MigrationLog { + level: MigrationLogLevel; + message: string; +} + +/** @internal */ +export interface Progress { + processed: number | undefined; + total: number | undefined; +} diff --git a/src/core/server/saved_objects/migrationsv2/README.md b/src/core/server/saved_objects/migrationsv2/README.md deleted file mode 100644 index 60bf84eef87a6..0000000000000 --- a/src/core/server/saved_objects/migrationsv2/README.md +++ /dev/null @@ -1,504 +0,0 @@ -- [Introduction](#introduction) -- [Algorithm steps](#algorithm-steps) - - [INIT](#init) - - [Next action](#next-action) - - [New control state](#new-control-state) - - [CREATE_NEW_TARGET](#create_new_target) - - [Next action](#next-action-1) - - [New control state](#new-control-state-1) - - [LEGACY_SET_WRITE_BLOCK](#legacy_set_write_block) - - [Next action](#next-action-2) - - [New control state](#new-control-state-2) - - [LEGACY_CREATE_REINDEX_TARGET](#legacy_create_reindex_target) - - [Next action](#next-action-3) - - [New control state](#new-control-state-3) - - [LEGACY_REINDEX](#legacy_reindex) - - [Next action](#next-action-4) - - [New control state](#new-control-state-4) - - [LEGACY_REINDEX_WAIT_FOR_TASK](#legacy_reindex_wait_for_task) - - [Next action](#next-action-5) - - [New control state](#new-control-state-5) - - [LEGACY_DELETE](#legacy_delete) - - [Next action](#next-action-6) - - [New control state](#new-control-state-6) - - [WAIT_FOR_YELLOW_SOURCE](#wait_for_yellow_source) - - [Next action](#next-action-7) - - [New control state](#new-control-state-7) - - [SET_SOURCE_WRITE_BLOCK](#set_source_write_block) - - [Next action](#next-action-8) - - [New control state](#new-control-state-8) - - [CREATE_REINDEX_TEMP](#create_reindex_temp) - - [Next action](#next-action-9) - - [New control state](#new-control-state-9) - - [REINDEX_SOURCE_TO_TEMP_OPEN_PIT](#reindex_source_to_temp_open_pit) - - [Next action](#next-action-10) - - [New control state](#new-control-state-10) - - [REINDEX_SOURCE_TO_TEMP_READ](#reindex_source_to_temp_read) - - [Next action](#next-action-11) - - [New control state](#new-control-state-11) - - [REINDEX_SOURCE_TO_TEMP_TRANSFORM](#REINDEX_SOURCE_TO_TEMP_TRANSFORM) - - [Next action](#next-action-12) - - [New control state](#new-control-state-12) - - [REINDEX_SOURCE_TO_TEMP_INDEX_BULK](#reindex_source_to_temp_index_bulk) - - [Next action](#next-action-13) - - [New control state](#new-control-state-13) - - [REINDEX_SOURCE_TO_TEMP_CLOSE_PIT](#reindex_source_to_temp_close_pit) - - [Next action](#next-action-14) - - [New control state](#new-control-state-14) - - [SET_TEMP_WRITE_BLOCK](#set_temp_write_block) - - [Next action](#next-action-15) - - [New control state](#new-control-state-15) - - [CLONE_TEMP_TO_TARGET](#clone_temp_to_target) - - [Next action](#next-action-16) - - [New control state](#new-control-state-16) - - [OUTDATED_DOCUMENTS_SEARCH](#outdated_documents_search) - - [Next action](#next-action-17) - - [New control state](#new-control-state-17) - - [OUTDATED_DOCUMENTS_TRANSFORM](#outdated_documents_transform) - - [Next action](#next-action-18) - - [New control state](#new-control-state-18) - - [UPDATE_TARGET_MAPPINGS](#update_target_mappings) - - [Next action](#next-action-19) - - [New control state](#new-control-state-19) - - [UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK](#update_target_mappings_wait_for_task) - - [Next action](#next-action-20) - - [New control state](#new-control-state-20) - - [MARK_VERSION_INDEX_READY_CONFLICT](#mark_version_index_ready_conflict) - - [Next action](#next-action-21) - - [New control state](#new-control-state-21) -- [Manual QA Test Plan](#manual-qa-test-plan) - - [1. Legacy pre-migration](#1-legacy-pre-migration) - - [2. Plugins enabled/disabled](#2-plugins-enableddisabled) - - [Test scenario 1 (enable a plugin after migration):](#test-scenario-1-enable-a-plugin-after-migration) - - [Test scenario 2 (disable a plugin after migration):](#test-scenario-2-disable-a-plugin-after-migration) - - [Test scenario 3 (multiple instances, enable a plugin after migration):](#test-scenario-3-multiple-instances-enable-a-plugin-after-migration) - - [Test scenario 4 (multiple instances, mixed plugin enabled configs):](#test-scenario-4-multiple-instances-mixed-plugin-enabled-configs) - -# Introduction -In the past, the risk of downtime caused by Kibana's saved object upgrade -migrations have discouraged users from adopting the latest features. v2 -migrations aims to solve this problem by minimizing the operational impact on -our users. - -To achieve this it uses a new migration algorithm where every step of the -algorithm is idempotent. No matter at which step a Kibana instance gets -interrupted, it can always restart the migration from the beginning and repeat -all the steps without requiring any user intervention. This doesn't mean -migrations will never fail, but when they fail for intermittent reasons like -an Elasticsearch cluster running out of heap, Kibana will automatically be -able to successfully complete the migration once the cluster has enough heap. - -For more background information on the problem see the [saved object -migrations -RFC](https://github.com/elastic/kibana/blob/main/rfcs/text/0013_saved_object_migrations.md). - -# Algorithm steps -The design goals for the algorithm was to keep downtime below 10 minutes for -100k saved objects while guaranteeing no data loss and keeping steps as simple -and explicit as possible. - -The algorithm is implemented as a state-action machine based on https://www.microsoft.com/en-us/research/uploads/prod/2016/12/Computation-and-State-Machines.pdf - -The state-action machine defines it's behaviour in steps. Each step is a -transition from a control state s_i to the contral state s_i+1 caused by an -action a_i. - -``` -s_i -> a_i -> s_i+1 -s_i+1 -> a_i+1 -> s_i+2 -``` - -Given a control state s1, `next(s1)` returns the next action to execute. -Actions are asynchronous, once the action resolves, we can use the action -response to determine the next state to transition to as defined by the -function `model(state, response)`. - -We can then loosely define a step as: -``` -s_i+1 = model(s_i, await next(s_i)()) -``` - -When there are no more actions returned by `next` the state-action machine -terminates such as in the DONE and FATAL control states. - -What follows is a list of all control states. For each control state the -following is described: - - _next action_: the next action triggered by the current control state - - _new control state_: based on the action response, the possible new control states that the machine will transition to - -Since the algorithm runs once for each saved object index the steps below -always reference a single saved object index `.kibana`. When Kibana starts up, -all the steps are also repeated for the `.kibana_task_manager` index but this -is left out of the description for brevity. - -## INIT -### Next action -`fetchIndices` - -Fetch the saved object indices, mappings and aliases to find the source index -and determine whether we’re migrating from a legacy index or a v1 migrations -index. - -### New control state -1. If `.kibana` and the version specific aliases both exists and are pointing -to the same index. This version's migration has already been completed. Since -the same version could have plugins enabled at any time that would introduce -new transforms or mappings. - → `OUTDATED_DOCUMENTS_SEARCH` - -2. If `.kibana` is pointing to an index that belongs to a later version of -Kibana .e.g. a 7.11.0 instance found the `.kibana` alias pointing to -`.kibana_7.12.0_001` fail the migration - → `FATAL` - -3. If the `.kibana` alias exists we’re migrating from either a v1 or v2 index -and the migration source index is the index the `.kibana` alias points to. - → `WAIT_FOR_YELLOW_SOURCE` - -4. If `.kibana` is a concrete index, we’re migrating from a legacy index - → `LEGACY_SET_WRITE_BLOCK` - -5. If there are no `.kibana` indices, this is a fresh deployment. Initialize a - new saved objects index - → `CREATE_NEW_TARGET` - -## CREATE_NEW_TARGET -### Next action -`createIndex` - -Create the target index. This operation is idempotent, if the index already exist, we wait until its status turns yellow - -### New control state - → `MARK_VERSION_INDEX_READY` - -## LEGACY_SET_WRITE_BLOCK -### Next action -`setWriteBlock` - -Set a write block on the legacy index to prevent any older Kibana instances -from writing to the index while the migration is in progress which could cause -lost acknowledged writes. - -This is the first of a series of `LEGACY_*` control states that will: - - reindex the concrete legacy `.kibana` index into a `.kibana_pre6.5.0_001` index - - delete the concrete `.kibana` _index_ so that we're able to create a `.kibana` _alias_ - -### New control state -1. If the write block was successfully added - → `LEGACY_CREATE_REINDEX_TARGET` -2. If the write block failed because the index doesn't exist, it means another instance already completed the legacy pre-migration. Proceed to the next step. - → `LEGACY_CREATE_REINDEX_TARGET` - -## LEGACY_CREATE_REINDEX_TARGET -### Next action -`createIndex` - -Create a new `.kibana_pre6.5.0_001` index into which we can reindex the legacy -index. (Since the task manager index was converted from a data index into a -saved objects index in 7.4 it will be reindexed into `.kibana_pre7.4.0_001`) -### New control state - → `LEGACY_REINDEX` - -## LEGACY_REINDEX -### Next action -`reindex` - -Let Elasticsearch reindex the legacy index into `.kibana_pre6.5.0_001`. (For -the task manager index we specify a `preMigrationScript` to convert the -original task manager documents into valid saved objects) -### New control state - → `LEGACY_REINDEX_WAIT_FOR_TASK` - - -## LEGACY_REINDEX_WAIT_FOR_TASK -### Next action -`waitForReindexTask` - -Wait for up to 60s for the reindex task to complete. -### New control state -1. If the reindex task completed - → `LEGACY_DELETE` -2. If the reindex task failed with a `target_index_had_write_block` or - `index_not_found_exception` another instance already completed this step - → `LEGACY_DELETE` -3. If the reindex task is still in progress - → `LEGACY_REINDEX_WAIT_FOR_TASK` - -## LEGACY_DELETE -### Next action -`updateAliases` - -Use the updateAliases API to atomically remove the legacy index and create a -new `.kibana` alias that points to `.kibana_pre6.5.0_001`. -### New control state -1. If the action succeeds - → `SET_SOURCE_WRITE_BLOCK` -2. If the action fails with `remove_index_not_a_concrete_index` or - `index_not_found_exception` another instance has already completed this step. - → `SET_SOURCE_WRITE_BLOCK` - -## WAIT_FOR_YELLOW_SOURCE -### Next action -`waitForIndexStatusYellow` - -Wait for the Elasticsearch cluster to be in "yellow" state. It means the index's primary shard is allocated and the index is ready for searching/indexing documents, but ES wasn't able to allocate the replicas. -We don't have as much data redundancy as we could have, but it's enough to start the migration. - -### New control state - → `SET_SOURCE_WRITE_BLOCK` - -## SET_SOURCE_WRITE_BLOCK -### Next action -`setWriteBlock` - -Set a write block on the source index to prevent any older Kibana instances from writing to the index while the migration is in progress which could cause lost acknowledged writes. - -### New control state - → `CREATE_REINDEX_TEMP` - -## CREATE_REINDEX_TEMP -### Next action -`createIndex` - -This operation is idempotent, if the index already exist, we wait until its status turns yellow. - -- Because we will be transforming documents before writing them into this index, we can already set the mappings to the target mappings for this version. The source index might contain documents belonging to a disabled plugin. So set `dynamic: false` mappings for any unknown saved object types. -- (Since we never query the temporary index we can potentially disable refresh to speed up indexing performance. Profile to see if gains justify complexity) - -### New control state - → `REINDEX_SOURCE_TO_TEMP_OPEN_PIT` - -## REINDEX_SOURCE_TO_TEMP_OPEN_PIT -### Next action -`openPIT` - -Open a PIT. Since there is a write block on the source index there is basically no overhead to keeping the PIT so we can lean towards a larger `keep_alive` value like 10 minutes. -### New control state - → `REINDEX_SOURCE_TO_TEMP_READ` - -## REINDEX_SOURCE_TO_TEMP_READ -### Next action -`readNextBatchOfSourceDocuments` - -Read the next batch of outdated documents from the source index by using search after with our PIT. - -### New control state -1. If the batch contained > 0 documents - → `REINDEX_SOURCE_TO_TEMP_TRANSFORM` -2. If there are no more documents returned - → `REINDEX_SOURCE_TO_TEMP_CLOSE_PIT` - -## REINDEX_SOURCE_TO_TEMP_TRANSFORM -### Next action -`transformRawDocs` - -Transform the current batch of documents - -In order to support sharing saved objects to multiple spaces in 8.0, the -transforms will also regenerate document `_id`'s. To ensure that this step -remains idempotent, the new `_id` is deterministically generated using UUIDv5 -ensuring that each Kibana instance generates the same new `_id` for the same document. -### New control state - → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` -## REINDEX_SOURCE_TO_TEMP_INDEX_BULK -### Next action -`bulkIndexTransformedDocuments` - -Use the bulk API create action to write a batch of up-to-date documents. The -create action ensures that there will be only one write per reindexed document -even if multiple Kibana instances are performing this step. Use -`refresh=false` to speed up the create actions, the `UPDATE_TARGET_MAPPINGS` -step will ensure that the index is refreshed before we start serving traffic. - -The following errors are ignored because it means another instance already -completed this step: - - documents already exist in the temp index - - temp index has a write block - - temp index is not found -### New control state -1. If `currentBatch` is the last batch in `transformedDocBatches` - → `REINDEX_SOURCE_TO_TEMP_READ` -2. If there are more batches left in `transformedDocBatches` - → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` - -## REINDEX_SOURCE_TO_TEMP_CLOSE_PIT -### Next action -`closePIT` - -### New control state - → `SET_TEMP_WRITE_BLOCK` - -## SET_TEMP_WRITE_BLOCK -### Next action -`setWriteBlock` - -Set a write block on the temporary index so that we can clone it. -### New control state - → `CLONE_TEMP_TO_TARGET` - -## CLONE_TEMP_TO_TARGET -### Next action -`cloneIndex` - -Ask elasticsearch to clone the temporary index into the target index. If the target index already exists (because another node already started the clone operation), wait until the clone is complete by waiting for a yellow index status. - -We can’t use the temporary index as our target index because one instance can complete the migration, delete a document, and then a second instance starts the reindex operation and re-creates the deleted document. By cloning the temporary index and only accepting writes/deletes from the cloned target index, we prevent lost acknowledged deletes. - -### New control state - → `OUTDATED_DOCUMENTS_SEARCH` - -## OUTDATED_DOCUMENTS_SEARCH -### Next action -`searchForOutdatedDocuments` - -Search for outdated saved object documents. Will return one batch of -documents. - -If another instance has a disabled plugin it will reindex that plugin's -documents without transforming them. Because this instance doesn't know which -plugins were disabled by the instance that performed the -`REINDEX_SOURCE_TO_TEMP_TRANSFORM` step, we need to search for outdated documents -and transform them to ensure that everything is up to date. - -### New control state -1. Found outdated documents? - → `OUTDATED_DOCUMENTS_TRANSFORM` -2. All documents up to date - → `UPDATE_TARGET_MAPPINGS` - -## OUTDATED_DOCUMENTS_TRANSFORM -### Next action -`transformRawDocs` + `bulkOverwriteTransformedDocuments` - -Once transformed we use an index operation to overwrite the outdated document with the up-to-date version. Optimistic concurrency control ensures that we only overwrite the document once so that any updates/writes by another instance which already completed the migration aren’t overwritten and lost. - -### New control state - → `OUTDATED_DOCUMENTS_SEARCH` - -## UPDATE_TARGET_MAPPINGS -### Next action -`updateAndPickupMappings` - -If another instance has some plugins disabled it will disable the mappings of that plugin's types when creating the temporary index. This action will -update the mappings and then use an update_by_query to ensure that all fields are “picked-up” and ready to be searched over. - -### New control state - → `UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK` - -## UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK -### Next action -`updateAliases` - -Atomically apply the `versionIndexReadyActions` using the _alias actions API. By performing the following actions we guarantee that if multiple versions of Kibana started the upgrade in parallel, only one version will succeed. - -1. verify that the current alias is still pointing to the source index -2. Point the version alias and the current alias to the target index. -3. Remove the temporary index - -### New control state -1. If all the actions succeed we’re ready to serve traffic - → `DONE` -2. If action (1) fails with alias_not_found_exception or action (3) fails with index_not_found_exception another instance already completed the migration - → `MARK_VERSION_INDEX_READY_CONFLICT` - -## MARK_VERSION_INDEX_READY_CONFLICT -### Next action -`fetchIndices` - -Fetch the saved object indices - -### New control state -If another instance completed a migration from the same source we need to verify that it is running the same version. - -1. If the current and version aliases are pointing to the same index the instance that completed the migration was on the same version and it’s safe to start serving traffic. - → `DONE` -2. If the other instance was running a different version we fail the migration. Once we restart one of two things can happen: the other instance is an older version and we will restart the migration, or, it’s a newer version and we will refuse to start up. - → `FATAL` - -# Manual QA Test Plan -## 1. Legacy pre-migration -When upgrading from a legacy index additional steps are required before the -regular migration process can start. - -We have the following potential legacy indices: - - v5.x index that wasn't upgraded -> kibana should refuse to start the migration - - v5.x index that was upgraded to v6.x: `.kibana-6` _index_ with `.kibana` _alias_ - - < v6.5 `.kibana` _index_ (Saved Object Migrations were - introduced in v6.5 https://github.com/elastic/kibana/pull/20243) - - TODO: Test versions which introduced the `kibana_index_template` template? - - < v7.4 `.kibana_task_manager` _index_ (Task Manager started - using Saved Objects in v7.4 https://github.com/elastic/kibana/pull/39829) - -Test plan: -1. Ensure that the different versions of Kibana listed above can successfully - upgrade to 7.11. -2. Ensure that multiple Kibana nodes can migrate a legacy index in parallel - (choose a representative legacy version to test with e.g. v6.4). Add a lot - of Saved Objects to Kibana to increase the time it takes for a migration to - complete which will make it easier to introduce failures. - 1. If all instances are started in parallel the upgrade should succeed - 2. If nodes are randomly restarted shortly after they start participating - in the migration the upgrade should either succeed or never complete. - However, if a fatal error occurs it should never result in permanent - failure. - 1. Start one instance, wait 500 ms - 2. Start a second instance - 3. If an instance starts a saved object migration, wait X ms before - killing the process and restarting the migration. - 4. Keep decreasing X until migrations are barely able to complete. - 5. If a migration fails with a fatal error, start a Kibana that doesn't - get restarted. Given enough time, it should always be able to - successfully complete the migration. - -For a successful migration the following behaviour should be observed: - 1. The `.kibana` index should be reindexed into a `.kibana_pre6.5.0` index - 2. The `.kibana` index should be deleted - 3. The `.kibana_index_template` should be deleted - 4. The `.kibana_pre6.5.0` index should have a write block applied - 5. Documents from `.kibana_pre6.5.0` should be migrated into `.kibana_7.11.0_001` - 6. Once migration has completed, the `.kibana_current` and `.kibana_7.11.0` - aliases should point to the `.kibana_7.11.0_001` index. - -## 2. Plugins enabled/disabled -Kibana plugins can be disabled/enabled at any point in time. We need to ensure -that Saved Object documents are migrated for all the possible sequences of -enabling, disabling, before or after a version upgrade. - -### Test scenario 1 (enable a plugin after migration): -1. Start an old version of Kibana (< 7.11) -2. Create a document that we know will be migrated in a later version (i.e. - create a `dashboard`) -3. Disable the plugin to which the document belongs (i.e `dashboard` plugin) -4. Upgrade Kibana to v7.11 making sure the plugin in step (3) is still disabled. -5. Enable the plugin from step (3) -6. Restart Kibana -7. Ensure that the document from step (2) has been migrated - (`migrationVersion` contains 7.11.0) - -### Test scenario 2 (disable a plugin after migration): -1. Start an old version of Kibana (< 7.11) -2. Create a document that we know will be migrated in a later version (i.e. - create a `dashboard`) -3. Upgrade Kibana to v7.11 making sure the plugin in step (3) is enabled. -4. Disable the plugin to which the document belongs (i.e `dashboard` plugin) -6. Restart Kibana -7. Ensure that Kibana logs a warning, but continues to start even though there - are saved object documents which don't belong to an enable plugin - -### Test scenario 3 (multiple instances, enable a plugin after migration): -Follow the steps from 'Test scenario 1', but perform the migration with -multiple instances of Kibana - -### Test scenario 4 (multiple instances, mixed plugin enabled configs): -We don't support this upgrade scenario, but it's worth making sure we don't -have data loss when there's a user error. -1. Start an old version of Kibana (< 7.11) -2. Create a document that we know will be migrated in a later version (i.e. - create a `dashboard`) -3. Disable the plugin to which the document belongs (i.e `dashboard` plugin) -4. Upgrade Kibana to v7.11 using multiple instances of Kibana. The plugin from - step (3) should be enabled on half of the instances and disabled on the - other half. -5. Ensure that the document from step (2) has been migrated - (`migrationVersion` contains 7.11.0) - diff --git a/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts b/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts index b12188347f8a7..b8b3a22c5d0fa 100644 --- a/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts +++ b/src/core/server/saved_objects/routes/integration_tests/migrate.test.mocks.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; +import { mockKibanaMigrator } from '../../migrations/kibana_migrator.mock'; export const migratorInstanceMock = mockKibanaMigrator.create(); export const KibanaMigratorMock = jest.fn().mockImplementation(() => migratorInstanceMock); -jest.doMock('../../migrations/kibana/kibana_migrator', () => ({ +jest.doMock('../../migrations/kibana_migrator', () => ({ KibanaMigrator: KibanaMigratorMock, })); diff --git a/src/core/server/saved_objects/saved_objects_service.test.mocks.ts b/src/core/server/saved_objects/saved_objects_service.test.mocks.ts index 1faebcc5fcc97..65273827122ec 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.mocks.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.mocks.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; +import { mockKibanaMigrator } from './migrations/kibana_migrator.mock'; import { savedObjectsClientProviderMock } from './service/lib/scoped_client_provider.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; export const migratorInstanceMock = mockKibanaMigrator.create(); export const KibanaMigratorMock = jest.fn().mockImplementation(() => migratorInstanceMock); -jest.doMock('./migrations/kibana/kibana_migrator', () => ({ +jest.doMock('./migrations/kibana_migrator', () => ({ KibanaMigrator: KibanaMigratorMock, })); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index baa1636dde13f..a55f370c7ca22 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -370,10 +370,10 @@ export class SavedObjectsService }; } - public async start( - { elasticsearch, pluginsInitialized = true }: SavedObjectsStartDeps, - migrationsRetryDelay?: number - ): Promise { + public async start({ + elasticsearch, + pluginsInitialized = true, + }: SavedObjectsStartDeps): Promise { if (!this.setupDeps || !this.config) { throw new Error('#setup() needs to be run first'); } @@ -384,8 +384,7 @@ export class SavedObjectsService const migrator = this.createMigrator( this.config.migration, - elasticsearch.client.asInternalUser, - migrationsRetryDelay + elasticsearch.client.asInternalUser ); this.migrator$.next(migrator); @@ -500,8 +499,7 @@ export class SavedObjectsService private createMigrator( soMigrationsConfig: SavedObjectsMigrationConfigType, - client: ElasticsearchClient, - migrationsRetryDelay?: number + client: ElasticsearchClient ): IKibanaMigrator { return new KibanaMigrator({ typeRegistry: this.typeRegistry, @@ -510,7 +508,6 @@ export class SavedObjectsService soMigrationsConfig, kibanaIndex, client, - migrationsRetryDelay, }); } diff --git a/src/core/server/saved_objects/service/lib/repository.test.ts b/src/core/server/saved_objects/service/lib/repository.test.ts index 8a9f099314b8c..46a532cdefef4 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.ts @@ -52,7 +52,7 @@ import { import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { DocumentMigrator } from '../../migrations/core/document_migrator'; -import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; +import { mockKibanaMigrator } from '../../migrations/kibana_migrator.mock'; import { LEGACY_URL_ALIAS_TYPE } from '../../object_types'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import * as esKuery from '@kbn/es-query'; diff --git a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts index a87f24a1eae14..2d03fff29df10 100644 --- a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts @@ -7,7 +7,7 @@ */ import { SavedObjectsRepository } from './repository'; -import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; +import { mockKibanaMigrator } from '../../migrations/kibana_migrator.mock'; import { KibanaMigrator } from '../../migrations'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { loggerMock, MockedLogger } from '../../../logging/logger.mock'; diff --git a/src/core/server/saved_objects/status.ts b/src/core/server/saved_objects/status.ts index 95bf6ddd9ff52..33cc344fc2b60 100644 --- a/src/core/server/saved_objects/status.ts +++ b/src/core/server/saved_objects/status.ts @@ -10,7 +10,7 @@ import { Observable, combineLatest } from 'rxjs'; import { startWith, map } from 'rxjs/operators'; import { ServiceStatus, ServiceStatusLevels } from '../status'; import { SavedObjectStatusMeta } from './types'; -import { KibanaMigratorStatus } from './migrations/kibana'; +import { KibanaMigratorStatus } from './migrations'; export const calculateStatus$ = ( rawMigratorStatus$: Observable, diff --git a/test/api_integration/apis/saved_objects/index.ts b/test/api_integration/apis/saved_objects/index.ts index 12189bce302b8..44ee3d8d7d76b 100644 --- a/test/api_integration/apis/saved_objects/index.ts +++ b/test/api_integration/apis/saved_objects/index.ts @@ -19,7 +19,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./import')); - loadTestFile(require.resolve('./migrations')); loadTestFile(require.resolve('./resolve')); loadTestFile(require.resolve('./resolve_import_errors')); loadTestFile(require.resolve('./update')); diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts deleted file mode 100644 index cba62ee51763d..0000000000000 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ /dev/null @@ -1,763 +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. - */ - -/* - * Smokescreen tests for core migration logic - */ - -import uuidv5 from 'uuid/v5'; -import { set } from '@elastic/safer-lodash-set'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import { SavedObjectsType } from 'src/core/server'; -import { Client as ElasticsearchClient } from '@elastic/elasticsearch'; - -import { - DocumentMigrator, - IndexMigrator, - createMigrationEsClient, -} from '../../../../src/core/server/saved_objects/migrations/core'; -import { SavedObjectsTypeMappingDefinitions } from '../../../../src/core/server/saved_objects/mappings'; - -import { - SavedObjectsSerializer, - SavedObjectTypeRegistry, -} from '../../../../src/core/server/saved_objects'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const KIBANA_VERSION = '99.9.9'; -const FOO_TYPE: SavedObjectsType = { - name: 'foo', - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, -}; -const BAR_TYPE: SavedObjectsType = { - name: 'bar', - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, -}; -const BAZ_TYPE: SavedObjectsType = { - name: 'baz', - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, -}; -const FLEET_AGENT_EVENT_TYPE: SavedObjectsType = { - name: 'fleet-agent-event', - hidden: false, - namespaceType: 'single', - mappings: { properties: {} }, -}; - -function getLogMock() { - return { - debug() {}, - error() {}, - fatal() {}, - info() {}, - log() {}, - trace() {}, - warn() {}, - get: getLogMock, - }; -} -export default ({ getService }: FtrProviderContext) => { - const esClient = getService('es'); - const esDeleteAllIndices = getService('esDeleteAllIndices'); - - describe('Kibana index migration', () => { - before(() => esDeleteAllIndices('.migrate-*')); - - it('Migrates an existing index that has never been migrated before', async () => { - const index = '.migration-a'; - const originalDocs = [ - { id: 'foo:a', type: 'foo', foo: { name: 'Foo A' } }, - { id: 'foo:e', type: 'foo', foo: { name: 'Fooey' } }, - { id: 'bar:i', type: 'bar', bar: { nomnom: 33 } }, - { id: 'bar:o', type: 'bar', bar: { nomnom: 2 } }, - { id: 'baz:u', type: 'baz', baz: { title: 'Terrific!' } }, - ]; - - const mappingProperties = { - foo: { properties: { name: { type: 'text' } } }, - bar: { properties: { mynum: { type: 'integer' } } }, - } as const; - - const savedObjectTypes: SavedObjectsType[] = [ - { - ...FOO_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), - }, - }, - { - ...BAR_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), - '1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }), - '1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2), - }, - }, - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - // Test that unrelated index templates are unaffected - await esClient.indices.putTemplate({ - name: 'migration_test_a_template', - body: { - index_patterns: ['migration_test_a'], - mappings: { - dynamic: 'strict', - properties: { baz: { type: 'text' } }, - }, - }, - }); - - // Test that obsolete index templates get removed - await esClient.indices.putTemplate({ - name: 'migration_a_template', - body: { - index_patterns: [index], - mappings: { - dynamic: 'strict', - properties: { baz: { type: 'text' } }, - }, - }, - }); - - const migrationATemplate = await esClient.indices.existsTemplate({ - name: 'migration_a_template', - }); - expect(migrationATemplate).to.be.ok(); - - const result = await migrateIndex({ - esClient, - index, - savedObjectTypes, - mappingProperties, - obsoleteIndexTemplatePattern: 'migration_a*', - }); - - const migrationATemplateAfter = await esClient.indices.existsTemplate({ - name: 'migration_a_template', - }); - - expect(migrationATemplateAfter).not.to.be.ok(); - const migrationTestATemplateAfter = await esClient.indices.existsTemplate({ - name: 'migration_test_a_template', - }); - - expect(migrationTestATemplateAfter).to.be.ok(); - expect(_.omit(result, 'elapsedMs')).to.eql({ - destIndex: '.migration-a_2', - sourceIndex: '.migration-a_1', - status: 'migrated', - }); - - // The docs in the original index are unchanged - expect(await fetchDocs(esClient, `${index}_1`)).to.eql(originalDocs.sort(sortByTypeAndId)); - - // The docs in the alias have been migrated - expect(await fetchDocs(esClient, index)).to.eql([ - { - id: 'bar:i', - type: 'bar', - migrationVersion: { bar: '1.9.0' }, - bar: { mynum: 68 }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:o', - type: 'bar', - migrationVersion: { bar: '1.9.0' }, - bar: { mynum: 6 }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'baz:u', - type: 'baz', - baz: { title: 'Terrific!' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:a', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'FOO A' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:e', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'FOOEY' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - }); - - it('migrates a previously migrated index, if migrations change', async () => { - const index = '.migration-b'; - const originalDocs = [ - { id: 'foo:a', type: 'foo', foo: { name: 'Foo A' } }, - { id: 'foo:e', type: 'foo', foo: { name: 'Fooey' } }, - { id: 'bar:i', type: 'bar', bar: { nomnom: 33 } }, - { id: 'bar:o', type: 'bar', bar: { nomnom: 2 } }, - ]; - - const mappingProperties = { - foo: { properties: { name: { type: 'text' } } }, - bar: { properties: { mynum: { type: 'integer' } } }, - } as const; - - let savedObjectTypes: SavedObjectsType[] = [ - { - ...FOO_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), - }, - }, - { - ...BAR_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), - '1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }), - '1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2), - }, - }, - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); - - // @ts-expect-error name doesn't exist on mynum type - mappingProperties.bar.properties.name = { type: 'keyword' }; - savedObjectTypes = [ - { - ...FOO_TYPE, - migrations: { - '2.0.1': (doc) => set(doc, 'attributes.name', `${doc.attributes.name}v2`), - }, - }, - { - ...BAR_TYPE, - migrations: { - '2.3.4': (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`), - }, - }, - ]; - - await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); - - // The index for the initial migration has not been destroyed... - expect(await fetchDocs(esClient, `${index}_2`)).to.eql([ - { - id: 'bar:i', - type: 'bar', - migrationVersion: { bar: '1.9.0' }, - bar: { mynum: 68 }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:o', - type: 'bar', - migrationVersion: { bar: '1.9.0' }, - bar: { mynum: 6 }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:a', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'FOO A' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:e', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'FOOEY' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - - // The docs were migrated again... - expect(await fetchDocs(esClient, index)).to.eql([ - { - id: 'bar:i', - type: 'bar', - migrationVersion: { bar: '2.3.4' }, - bar: { mynum: 68, name: 'NAME i' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:o', - type: 'bar', - migrationVersion: { bar: '2.3.4' }, - bar: { mynum: 6, name: 'NAME o' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:a', - type: 'foo', - migrationVersion: { foo: '2.0.1' }, - foo: { name: 'FOO Av2' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'foo:e', - type: 'foo', - migrationVersion: { foo: '2.0.1' }, - foo: { name: 'FOOEYv2' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - }); - - it('drops fleet-agent-event saved object types when doing a migration', async () => { - const index = '.migration-b'; - const originalDocs = [ - { - id: 'fleet-agent-event:a', - type: 'fleet-agent-event', - 'fleet-agent-event': { name: 'Foo A' }, - }, - { - id: 'fleet-agent-event:e', - type: 'fleet-agent-event', - 'fleet-agent-event': { name: 'Fooey' }, - }, - { id: 'bar:i', type: 'bar', bar: { nomnom: 33 } }, - { id: 'bar:o', type: 'bar', bar: { nomnom: 2 } }, - ]; - - const mappingProperties = { - 'fleet-agent-event': { properties: { name: { type: 'text' } } }, - bar: { properties: { mynum: { type: 'integer' } } }, - } as const; - - let savedObjectTypes: SavedObjectsType[] = [ - FLEET_AGENT_EVENT_TYPE, - { - ...BAR_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), - '1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }), - '1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2), - }, - }, - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); - - // @ts-expect-error name doesn't exist on mynum type - mappingProperties.bar.properties.name = { type: 'keyword' }; - savedObjectTypes = [ - FLEET_AGENT_EVENT_TYPE, - { - ...BAR_TYPE, - migrations: { - '2.3.4': (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`), - }, - }, - ]; - - await migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }); - - // Assert that fleet-agent-events were dropped - expect(await fetchDocs(esClient, index)).to.eql([ - { - id: 'bar:i', - type: 'bar', - migrationVersion: { bar: '2.3.4' }, - bar: { mynum: 68, name: 'NAME i' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:o', - type: 'bar', - migrationVersion: { bar: '2.3.4' }, - bar: { mynum: 6, name: 'NAME o' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - }); - - it('Coordinates migrations across the Kibana cluster', async () => { - const index = '.migration-c'; - const originalDocs = [{ id: 'foo:lotr', type: 'foo', foo: { name: 'Lord of the Rings' } }]; - - const mappingProperties = { - foo: { properties: { name: { type: 'text' } } }, - } as const; - - const savedObjectTypes: SavedObjectsType[] = [ - { - ...FOO_TYPE, - migrations: { - '1.0.0': (doc) => set(doc, 'attributes.name', 'LOTR'), - }, - }, - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - const result = await Promise.all([ - migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }), - migrateIndex({ esClient, index, savedObjectTypes, mappingProperties }), - ]); - - // The polling instance and the migrating instance should both - // return a similar migration result. - expect( - result - // @ts-expect-error destIndex exists only on MigrationResult status: 'migrated'; - .map(({ status, destIndex }) => ({ status, destIndex })) - .sort(({ destIndex: a }, { destIndex: b }) => - // sort by destIndex in ascending order, keeping falsy values at the end - (a && !b) || a < b ? -1 : (!a && b) || a > b ? 1 : 0 - ) - ).to.eql([ - { status: 'migrated', destIndex: '.migration-c_2' }, - { status: 'skipped', destIndex: undefined }, - ]); - - const body = await esClient.cat.indices({ index: '.migration-c*', format: 'json' }); - // It only created the original and the dest - expect(_.map(body, 'index').sort()).to.eql(['.migration-c_1', '.migration-c_2']); - - // The docs in the original index are unchanged - expect(await fetchDocs(esClient, `${index}_1`)).to.eql([ - { id: 'foo:lotr', type: 'foo', foo: { name: 'Lord of the Rings' } }, - ]); - - // The docs in the alias have been migrated - expect(await fetchDocs(esClient, index)).to.eql([ - { - id: 'foo:lotr', - type: 'foo', - migrationVersion: { foo: '1.0.0' }, - foo: { name: 'LOTR' }, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - ]); - }); - - it('Correctly applies reference transforms and conversion transforms', async () => { - const index = '.migration-d'; - const originalDocs = [ - { id: 'foo:1', type: 'foo', foo: { name: 'Foo 1 default' } }, - { id: 'spacex:foo:1', type: 'foo', foo: { name: 'Foo 1 spacex' }, namespace: 'spacex' }, - { - id: 'bar:1', - type: 'bar', - bar: { nomnom: 1 }, - references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], - }, - { - id: 'spacex:bar:1', - type: 'bar', - bar: { nomnom: 2 }, - references: [{ type: 'foo', id: '1', name: 'Foo 1 spacex' }], - namespace: 'spacex', - }, - { - id: 'baz:1', - type: 'baz', - baz: { title: 'Baz 1 default' }, - references: [{ type: 'bar', id: '1', name: 'Bar 1 default' }], - }, - { - id: 'spacex:baz:1', - type: 'baz', - baz: { title: 'Baz 1 spacex' }, - references: [{ type: 'bar', id: '1', name: 'Bar 1 spacex' }], - namespace: 'spacex', - }, - ]; - - const mappingProperties = { - foo: { properties: { name: { type: 'text' } } }, - bar: { properties: { nomnom: { type: 'integer' } } }, - baz: { properties: { title: { type: 'keyword' } } }, - } as const; - - const savedObjectTypes: SavedObjectsType[] = [ - { - ...FOO_TYPE, - namespaceType: 'multiple', - convertToMultiNamespaceTypeVersion: '1.0.0', - }, - { - ...BAR_TYPE, - namespaceType: 'multiple-isolated', - convertToMultiNamespaceTypeVersion: '2.0.0', - }, - BAZ_TYPE, // must be registered for reference transforms to be applied to objects of this type - ]; - - await createIndex({ esClient, index, esDeleteAllIndices }); - await createDocs({ esClient, index, docs: originalDocs }); - - await migrateIndex({ - esClient, - index, - savedObjectTypes, - mappingProperties, - obsoleteIndexTemplatePattern: 'migration_a*', - }); - - // The docs in the original index are unchanged - expect(await fetchDocs(esClient, `${index}_1`)).to.eql(originalDocs.sort(sortByTypeAndId)); - - // The docs in the alias have been migrated - const migratedDocs = await fetchDocs(esClient, index); - - // each newly converted multi-namespace object in a non-default space has its ID deterministically regenerated, and a legacy-url-alias - // object is created which links the old ID to the new ID - const newFooId = uuidv5('spacex:foo:1', uuidv5.DNS); - const newBarId = uuidv5('spacex:bar:1', uuidv5.DNS); - - expect(migratedDocs).to.eql( - [ - { - id: 'foo:1', - type: 'foo', - foo: { name: 'Foo 1 default' }, - references: [], - namespaces: ['default'], - migrationVersion: { foo: '1.0.0' }, - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: `foo:${newFooId}`, - type: 'foo', - foo: { name: 'Foo 1 spacex' }, - references: [], - namespaces: ['spacex'], - originId: '1', - migrationVersion: { foo: '1.0.0' }, - coreMigrationVersion: KIBANA_VERSION, - }, - { - // new object - id: 'legacy-url-alias:spacex:foo:1', - type: 'legacy-url-alias', - 'legacy-url-alias': { - sourceId: '1', - targetId: newFooId, - targetNamespace: 'spacex', - targetType: 'foo', - }, - migrationVersion: {}, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'bar:1', - type: 'bar', - bar: { nomnom: 1 }, - references: [{ type: 'foo', id: '1', name: 'Foo 1 default' }], - namespaces: ['default'], - migrationVersion: { bar: '2.0.0' }, - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: `bar:${newBarId}`, - type: 'bar', - bar: { nomnom: 2 }, - references: [{ type: 'foo', id: newFooId, name: 'Foo 1 spacex' }], - namespaces: ['spacex'], - originId: '1', - migrationVersion: { bar: '2.0.0' }, - coreMigrationVersion: KIBANA_VERSION, - }, - { - // new object - id: 'legacy-url-alias:spacex:bar:1', - type: 'legacy-url-alias', - 'legacy-url-alias': { - sourceId: '1', - targetId: newBarId, - targetNamespace: 'spacex', - targetType: 'bar', - }, - migrationVersion: {}, - references: [], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'baz:1', - type: 'baz', - baz: { title: 'Baz 1 default' }, - references: [{ type: 'bar', id: '1', name: 'Bar 1 default' }], - coreMigrationVersion: KIBANA_VERSION, - }, - { - id: 'spacex:baz:1', - type: 'baz', - baz: { title: 'Baz 1 spacex' }, - references: [{ type: 'bar', id: newBarId, name: 'Bar 1 spacex' }], - namespace: 'spacex', - coreMigrationVersion: KIBANA_VERSION, - }, - ].sort(sortByTypeAndId) - ); - }); - }); -}; - -async function createIndex({ - esClient, - index, - esDeleteAllIndices, -}: { - esClient: ElasticsearchClient; - index: string; - esDeleteAllIndices: (pattern: string) => Promise; -}) { - await esDeleteAllIndices(`${index}*`); - - const properties = { - type: { type: 'keyword' }, - foo: { properties: { name: { type: 'keyword' } } }, - bar: { properties: { nomnom: { type: 'integer' } } }, - baz: { properties: { title: { type: 'keyword' } } }, - 'legacy-url-alias': { - properties: { - targetNamespace: { type: 'text' }, - targetType: { type: 'text' }, - targetId: { type: 'text' }, - lastResolved: { type: 'date' }, - resolveCounter: { type: 'integer' }, - disabled: { type: 'boolean' }, - }, - }, - namespace: { type: 'keyword' }, - namespaces: { type: 'keyword' }, - originId: { type: 'keyword' }, - references: { - type: 'nested', - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - id: { type: 'keyword' }, - }, - }, - coreMigrationVersion: { - type: 'keyword', - }, - } as const; - await esClient.indices.create({ - index, - body: { mappings: { dynamic: 'strict', properties } }, - }); -} - -async function createDocs({ - esClient, - index, - docs, -}: { - esClient: ElasticsearchClient; - index: string; - docs: any[]; -}) { - await esClient.bulk({ - body: docs.reduce((acc, doc) => { - acc.push({ index: { _id: doc.id, _index: index } }); - acc.push(_.omit(doc, 'id')); - return acc; - }, []), - }); - await esClient.indices.refresh({ index }); -} - -async function migrateIndex({ - esClient, - index, - savedObjectTypes, - mappingProperties, - obsoleteIndexTemplatePattern, -}: { - esClient: ElasticsearchClient; - index: string; - savedObjectTypes: SavedObjectsType[]; - mappingProperties: SavedObjectsTypeMappingDefinitions; - obsoleteIndexTemplatePattern?: string; -}) { - const typeRegistry = new SavedObjectTypeRegistry(); - savedObjectTypes.forEach((type) => typeRegistry.registerType(type)); - - const documentMigrator = new DocumentMigrator({ - kibanaVersion: KIBANA_VERSION, - typeRegistry, - minimumConvertVersion: '0.0.0', // bypass the restriction of a minimum version of 8.0.0 for these integration tests - log: getLogMock(), - }); - - documentMigrator.prepareMigrations(); - - const migrator = new IndexMigrator({ - client: createMigrationEsClient(esClient, getLogMock()), - documentMigrator, - index, - kibanaVersion: KIBANA_VERSION, - obsoleteIndexTemplatePattern, - mappingProperties, - batchSize: 10, - log: getLogMock(), - setStatus: () => {}, - pollInterval: 50, - scrollDuration: '5m', - serializer: new SavedObjectsSerializer(typeRegistry), - }); - - return await migrator.migrate(); -} - -async function fetchDocs(esClient: ElasticsearchClient, index: string) { - const body = await esClient.search({ index }); - - return body.hits.hits - .map((h) => ({ - ...h._source, - id: h._id, - })) - .sort(sortByTypeAndId); -} - -function sortByTypeAndId(a: { type: string; id: string }, b: { type: string; id: string }) { - return a.type.localeCompare(b.type) || a.id.localeCompare(b.id); -} From cecfc358ab99ded603d9cd5528a8b356d4ad1b3d Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 10 Nov 2021 13:52:05 +0100 Subject: [PATCH 84/98] [Lens] Refactor little things in preparation for gauges (#117868) * change chart menu order, group and copy * add params to vis toolbar: handleClose() and panelClassname. Move Default classname to more appropriate place (not xy because it's used everywhere) * move supportStaticValue and supportFieldFormat on group level, add paramEditorCustomProps to pass label for reference line value. Refactor tabs * priority sorted out * CR Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../datatable_visualization/visualization.tsx | 4 +- .../editor_frame/config_panel/layer_panel.tsx | 13 +-- .../heatmap_visualization/visualization.tsx | 7 +- .../dimension_panel/dimension_editor.tsx | 110 ++++++++++-------- .../dimensions_editor_helpers.tsx | 59 +++------- .../dimension_panel/reference_editor.tsx | 5 +- .../operations/definitions/index.ts | 3 +- .../operations/definitions/static_value.tsx | 9 +- .../metric_visualization/visualization.tsx | 4 +- .../shared_components/toolbar_popover.scss | 3 + .../shared_components/toolbar_popover.tsx | 8 +- x-pack/plugins/lens/public/types.ts | 8 +- .../lens/public/xy_visualization/types.ts | 2 + .../xy_visualization/visualization.test.ts | 3 +- .../public/xy_visualization/visualization.tsx | 11 +- .../xy_config_panel/color_picker.tsx | 1 - .../xy_config_panel/index.tsx | 1 - .../xy_config_panel/layer_header.tsx | 1 - .../xy_config_panel/reference_line_panel.tsx | 1 - .../xy_config_panel/xy_config_panel.scss | 3 - 20 files changed, 123 insertions(+), 133 deletions(-) create mode 100644 x-pack/plugins/lens/public/shared_components/toolbar_popover.scss delete mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 807d32a245834..b21b8b8e07b36 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -49,9 +49,9 @@ export const getDatatableVisualization = ({ icon: LensIconChartDatatable, label: visualizationLabel, groupLabel: i18n.translate('xpack.lens.datatable.groupLabel', { - defaultMessage: 'Tabular and single value', + defaultMessage: 'Tabular', }), - sortPriority: 1, + sortPriority: 5, }, ], diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index bdd5d93c2c2c8..51d880e8f7c1c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -125,11 +125,7 @@ export function LayerPanel( dateRange, }; - const { - groups, - supportStaticValue, - supportFieldFormat = true, - } = useMemo( + const { groups } = useMemo( () => activeVisualization.getConfiguration(layerVisualizationConfigProps), // eslint-disable-next-line react-hooks/exhaustive-deps [ @@ -518,7 +514,7 @@ export function LayerPanel( setActiveDimension({ activeGroup: group, activeId: id, - isNew: !supportStaticValue, + isNew: !group.supportStaticValue, }); }} onDrop={onDrop} @@ -575,8 +571,9 @@ export function LayerPanel( toggleFullscreen, isFullscreen, setState: updateDataLayerState, - supportStaticValue: Boolean(supportStaticValue), - supportFieldFormat: Boolean(supportFieldFormat), + supportStaticValue: Boolean(activeGroup.supportStaticValue), + paramEditorCustomProps: activeGroup.paramEditorCustomProps, + supportFieldFormat: activeGroup.supportFieldFormat !== false, layerType: activeVisualization.getLayerType(layerId, visualizationState), }} /> diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx index 674af79db6c90..aa053d4aea06d 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx @@ -33,8 +33,8 @@ import { getSafePaletteParams } from './utils'; import type { CustomPaletteParams } from '../../common'; import { layerTypes } from '../../common'; -const groupLabelForBar = i18n.translate('xpack.lens.heatmapVisualization.heatmapGroupLabel', { - defaultMessage: 'Heatmap', +const groupLabelForHeatmap = i18n.translate('xpack.lens.heatmapVisualization.heatmapGroupLabel', { + defaultMessage: 'Magnitude', }); interface HeatmapVisualizationDeps { @@ -105,8 +105,9 @@ export const getHeatmapVisualization = ({ label: i18n.translate('xpack.lens.heatmapVisualization.heatmapLabel', { defaultMessage: 'Heatmap', }), - groupLabel: groupLabelForBar, + groupLabel: groupLabelForHeatmap, showExperimentalBadge: true, + sortPriority: 1, }, ], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 74628a31ea281..d34430e717e66 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -55,6 +55,7 @@ import { CalloutWarning, LabelInput, getErrorMessage, + DimensionEditorTab, } from './dimensions_editor_helpers'; import type { TemporaryState } from './dimensions_editor_helpers'; @@ -84,6 +85,7 @@ export function DimensionEditor(props: DimensionEditorProps) { supportStaticValue, supportFieldFormat = true, layerType, + paramEditorCustomProps, } = props; const services = { data: props.data, @@ -478,6 +480,7 @@ export function DimensionEditor(props: DimensionEditorProps) { isFullscreen={isFullscreen} toggleFullscreen={toggleFullscreen} setIsCloseable={setIsCloseable} + paramEditorCustomProps={paramEditorCustomProps} {...services} /> ); @@ -559,6 +562,7 @@ export function DimensionEditor(props: DimensionEditorProps) { toggleFullscreen={toggleFullscreen} isFullscreen={isFullscreen} setIsCloseable={setIsCloseable} + paramEditorCustomProps={paramEditorCustomProps} {...services} /> )} @@ -674,6 +678,7 @@ export function DimensionEditor(props: DimensionEditorProps) { toggleFullscreen={toggleFullscreen} isFullscreen={isFullscreen} setIsCloseable={setIsCloseable} + paramEditorCustomProps={paramEditorCustomProps} {...services} /> @@ -700,57 +705,64 @@ export function DimensionEditor(props: DimensionEditorProps) { const hasTabs = !isFullscreen && (hasFormula || supportStaticValue); + const tabs: DimensionEditorTab[] = [ + { + id: staticValueOperationName, + enabled: Boolean(supportStaticValue), + state: showStaticValueFunction, + onClick: () => { + if (selectedColumn?.operationType === formulaOperationName) { + return setTemporaryState(staticValueOperationName); + } + setTemporaryState('none'); + setStateWrapper(addStaticValueColumn()); + return; + }, + label: i18n.translate('xpack.lens.indexPattern.staticValueLabel', { + defaultMessage: 'Static value', + }), + }, + { + id: quickFunctionsName, + enabled: true, + state: showQuickFunctions, + onClick: () => { + if (selectedColumn && !isQuickFunction(selectedColumn.operationType)) { + setTemporaryState(quickFunctionsName); + return; + } + }, + label: i18n.translate('xpack.lens.indexPattern.quickFunctionsLabel', { + defaultMessage: 'Quick functions', + }), + }, + { + id: formulaOperationName, + enabled: hasFormula, + state: temporaryState === 'none' && selectedColumn?.operationType === formulaOperationName, + onClick: () => { + setTemporaryState('none'); + if (selectedColumn?.operationType !== formulaOperationName) { + const newLayer = insertOrReplaceColumn({ + layer: props.state.layers[props.layerId], + indexPattern: currentIndexPattern, + columnId, + op: formulaOperationName, + visualizationGroups: dimensionGroups, + }); + setStateWrapper(newLayer); + trackUiEvent(`indexpattern_dimension_operation_formula`); + } + }, + label: i18n.translate('xpack.lens.indexPattern.formulaLabel', { + defaultMessage: 'Formula', + }), + }, + ]; + return (
- {hasTabs ? ( - { - if (tabClicked === 'quickFunctions') { - if (selectedColumn && !isQuickFunction(selectedColumn.operationType)) { - setTemporaryState(quickFunctionsName); - return; - } - } - - if (tabClicked === 'static_value') { - // when coming from a formula, set a temporary state - if (selectedColumn?.operationType === formulaOperationName) { - return setTemporaryState(staticValueOperationName); - } - setTemporaryState('none'); - setStateWrapper(addStaticValueColumn()); - return; - } - - if (tabClicked === 'formula') { - setTemporaryState('none'); - if (selectedColumn?.operationType !== formulaOperationName) { - const newLayer = insertOrReplaceColumn({ - layer: props.state.layers[props.layerId], - indexPattern: currentIndexPattern, - columnId, - op: formulaOperationName, - visualizationGroups: dimensionGroups, - }); - setStateWrapper(newLayer); - trackUiEvent(`indexpattern_dimension_operation_formula`); - } - } - }} - /> - ) : null} - + {hasTabs ? : null} void; + id: typeof quickFunctionsName | typeof staticValueOperationName | typeof formulaOperationName; + label: string; +} -export const DimensionEditorTabs = ({ - tabsEnabled, - tabsState, - onClick, -}: { - tabsEnabled: Record; - tabsState: Record; - onClick: (tabClicked: DimensionEditorTabsType) => void; -}) => { +export const DimensionEditorTabs = ({ tabs }: { tabs: DimensionEditorTab[] }) => { return ( - {tabsEnabled.static_value ? ( - onClick(staticValueOperationName)} - > - {i18n.translate('xpack.lens.indexPattern.staticValueLabel', { - defaultMessage: 'Static value', - })} - - ) : null} - onClick(quickFunctionsName)} - > - {i18n.translate('xpack.lens.indexPattern.quickFunctionsLabel', { - defaultMessage: 'Quick functions', - })} - - {tabsEnabled.formula ? ( - onClick(formulaOperationName)} - > - {i18n.translate('xpack.lens.indexPattern.formulaLabel', { - defaultMessage: 'Formula', - })} - - ) : null} + {tabs.map(({ id, enabled, state, onClick, label }) => { + return enabled ? ( + + {label} + + ) : null; + })} ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index af8c8d7d1bf28..6fa1912effc2a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -34,7 +34,7 @@ import { FieldSelect } from './field_select'; import { hasField } from '../utils'; import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { VisualizationDimensionGroupConfig } from '../../types'; +import { ParamEditorCustomProps, VisualizationDimensionGroupConfig } from '../../types'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; const operationPanels = getOperationDisplay(); @@ -65,6 +65,7 @@ export interface ReferenceEditorProps { savedObjectsClient: SavedObjectsClientContract; http: HttpSetup; data: DataPublicPluginStart; + paramEditorCustomProps?: ParamEditorCustomProps; } export function ReferenceEditor(props: ReferenceEditorProps) { @@ -84,6 +85,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { isFullscreen, toggleFullscreen, setIsCloseable, + paramEditorCustomProps, ...services } = props; @@ -364,6 +366,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { isFullscreen={isFullscreen} toggleFullscreen={toggleFullscreen} setIsCloseable={setIsCloseable} + paramEditorCustomProps={paramEditorCustomProps} {...services} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 392b2b135ca22..5898cfc26d88c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -51,7 +51,7 @@ import { } from './formula'; import { staticValueOperation, StaticValueIndexPatternColumn } from './static_value'; import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; -import { FrameDatasourceAPI, OperationMetadata } from '../../../types'; +import { FrameDatasourceAPI, OperationMetadata, ParamEditorCustomProps } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types'; import { DateRange, LayerType } from '../../../../common'; @@ -197,6 +197,7 @@ export interface ParamEditorProps { data: DataPublicPluginStart; activeData?: IndexPatternDimensionEditorProps['activeData']; operationDefinitionMap: Record; + paramEditorCustomProps?: ParamEditorCustomProps; } export interface HelpProps { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index 26be4e7b114da..b759ebe46fb33 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -137,13 +137,12 @@ export const staticValueOperation: OperationDefinition< }, paramEditor: function StaticValueEditor({ - layer, updateLayer, currentColumn, columnId, activeData, layerId, - indexPattern, + paramEditorCustomProps, }) { const onChange = useCallback( (newValue) => { @@ -201,11 +200,7 @@ export const staticValueOperation: OperationDefinition< return (
- - {i18n.translate('xpack.lens.indexPattern.staticValue.label', { - defaultMessage: 'Reference line value', - })} - + {paramEditorCustomProps?.label || defaultLabel} = { defaultMessage: 'Metric', }), groupLabel: i18n.translate('xpack.lens.metric.groupLabel', { - defaultMessage: 'Tabular and single value', + defaultMessage: 'Single value', }), - sortPriority: 1, + sortPriority: 3, }, ], diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss b/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss new file mode 100644 index 0000000000000..a11e3373df467 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.scss @@ -0,0 +1,3 @@ +.lnsVisToolbar__popover { + width: 365px; +} diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx index 18c73a01cf784..e6bb2fcdc0825 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import './toolbar_popover.scss'; import React, { useState } from 'react'; import { EuiFlexItem, EuiPopover, EuiIcon, EuiPopoverTitle, IconType } from '@elastic/eui'; import { EuiIconLegend } from '../assets/legend'; @@ -36,6 +37,8 @@ export interface ToolbarPopoverProps { */ groupPosition?: ToolbarButtonProps['groupPosition']; buttonDataTestSubj?: string; + panelClassName?: string; + handleClose?: () => void; } export const ToolbarPopover: React.FunctionComponent = ({ @@ -45,6 +48,8 @@ export const ToolbarPopover: React.FunctionComponent = ({ isDisabled = false, groupPosition, buttonDataTestSubj, + panelClassName = 'lnsVisToolbar__popover', + handleClose, }) => { const [open, setOpen] = useState(false); @@ -53,7 +58,7 @@ export const ToolbarPopover: React.FunctionComponent = ({ return ( = ({ isOpen={open} closePopover={() => { setOpen(false); + handleClose?.(); }} anchorPosition="downRight" > diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 975e44f703959..a9a9539064659 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -338,7 +338,7 @@ export type DatasourceDimensionProps = SharedDimensionProps & { invalid?: boolean; invalidMessage?: string; }; - +export type ParamEditorCustomProps = Record & { label?: string }; // The only way a visualization has to restrict the query building export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { // Not a StateSetter because we have this unique use case of determining valid columns @@ -356,6 +356,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro isFullscreen: boolean; layerType: LayerType | undefined; supportStaticValue: boolean; + paramEditorCustomProps?: ParamEditorCustomProps; supportFieldFormat?: boolean; }; @@ -485,6 +486,9 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { invalidMessage?: string; // need a special flag to know when to pass the previous column on duplicating requiresPreviousColumnOnDuplicate?: boolean; + supportStaticValue?: boolean; + paramEditorCustomProps?: ParamEditorCustomProps; + supportFieldFormat?: boolean; }; interface VisualizationDimensionChangeProps { @@ -673,8 +677,6 @@ export interface Visualization { */ getConfiguration: (props: VisualizationConfigProps) => { groups: VisualizationDimensionGroupConfig[]; - supportStaticValue?: boolean; - supportFieldFormat?: boolean; }; /** diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 475571b2965f6..75e80782c5d38 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -69,6 +69,7 @@ export const visualizationTypes: VisualizationType[] = [ defaultMessage: 'Bar vertical', }), groupLabel: groupLabelForBar, + sortPriority: 4, }, { id: 'bar_horizontal', @@ -153,5 +154,6 @@ export const visualizationTypes: VisualizationType[] = [ defaultMessage: 'Line', }), groupLabel: groupLabelForLineAndArea, + sortPriority: 2, }, ]; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 973501816bc3e..0c3fa21708263 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -781,13 +781,12 @@ describe('xy_visualization', () => { const state = getStateWithBaseReferenceLine(); state.layers[0].accessors = []; state.layers[1].yConfig = undefined; - expect( xyVisualization.getConfiguration({ state: getStateWithBaseReferenceLine(), frame, layerId: 'referenceLine', - }).supportStaticValue + }).groups[0].supportStaticValue ).toBeTruthy(); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index c23eccb196744..2f3ec7e2723d4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -274,7 +274,7 @@ export const getXyVisualization = ({ getConfiguration({ state, frame, layerId }) { const layer = state.layers.find((l) => l.layerId === layerId); if (!layer) { - return { groups: [], supportStaticValue: true }; + return { groups: [] }; } const datasource = frame.datasourceLayers[layer.layerId]; @@ -345,8 +345,6 @@ export const getXyVisualization = ({ frame?.activeData ); return { - supportFieldFormat: false, - supportStaticValue: true, // Each reference lines layer panel will have sections for each available axis // (horizontal axis, vertical axis left, vertical axis right). // Only axes that support numeric reference lines should be shown @@ -362,6 +360,13 @@ export const getXyVisualization = ({ supportsMoreColumns: true, required: false, enableDimensionEditor: true, + supportStaticValue: true, + paramEditorCustomProps: { + label: i18n.translate('xpack.lens.indexPattern.staticValue.label', { + defaultMessage: 'Reference line value', + }), + }, + supportFieldFormat: false, dataTestSubj, invalid: !valid, invalidMessage: diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx index e3e53126015eb..517f4bd378591 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import './xy_config_panel.scss'; import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { debounce } from 'lodash'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx index e18ea18c30fb0..cef4a5f01ce8a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import './xy_config_panel.scss'; import React, { memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { Position, ScaleType, VerticalAlignment, HorizontalAlignment } from '@elastic/charts'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx index d81979f603943..5de54cecd2101 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import './xy_config_panel.scss'; import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiIcon, EuiPopover, EuiSelectable, EuiText, EuiPopoverTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx index 7b9fd01e540fe..f35bcae6ffb9f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import './xy_config_panel.scss'; import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss deleted file mode 100644 index a2caeb93477fa..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/xy_config_panel.scss +++ /dev/null @@ -1,3 +0,0 @@ -.lnsXyToolbar__popover { - width: 365px; -} From 94333460b51ea6317315ace3e45d6e977c440b2c Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 10 Nov 2021 16:12:17 +0300 Subject: [PATCH 85/98] [Timelion ]Remove usage of ignore_throttled unless targeting frozen indices to avoid deprecation warning (#118022) * [Timelion ]Remove usage of ignore_throttled unless targeting frozen indices to avoid deprecation warning Part of: #117980 * add tests --- .../timelion/server/series_functions/es/es.test.js | 12 ++++++++++-- .../server/series_functions/es/lib/build_request.js | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js b/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js index f55ee31f39799..9c0dac6f6975a 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js @@ -256,12 +256,20 @@ describe('es', () => { sandbox.restore(); }); - test('sets ignore_throttled=true on the request', () => { + test('sets ignore_throttled=false on the request', () => { + config.index = 'beer'; + tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN] = true; + const request = fn(config, tlConfig, emptyScriptFields); + + expect(request.params.ignore_throttled).toEqual(false); + }); + + test('sets no ignore_throttled if SEARCH_INCLUDE_FROZEN is false', () => { config.index = 'beer'; tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN] = false; const request = fn(config, tlConfig, emptyScriptFields); - expect(request.params.ignore_throttled).toEqual(true); + expect(request.params).not.toHaveProperty('ignore_throttled'); }); test('sets no timeout if elasticsearch.shardTimeout is set to 0', () => { diff --git a/src/plugins/vis_types/timelion/server/series_functions/es/lib/build_request.js b/src/plugins/vis_types/timelion/server/series_functions/es/lib/build_request.js index 20e3f71801854..99b5d0bacd858 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/es/lib/build_request.js +++ b/src/plugins/vis_types/timelion/server/series_functions/es/lib/build_request.js @@ -66,9 +66,10 @@ export default function buildRequest(config, tlConfig, scriptFields, runtimeFiel _.assign(aggCursor, createDateAgg(config, tlConfig, scriptFields)); + const includeFrozen = Boolean(tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN]); const request = { index: config.index, - ignore_throttled: !tlConfig.settings[UI_SETTINGS.SEARCH_INCLUDE_FROZEN], + ...(includeFrozen ? { ignore_throttled: false } : {}), body: { query: { bool: bool, From 49fe04196aa8b9e9129fea96dbdc3fb32c8a6092 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 10 Nov 2021 13:13:09 +0000 Subject: [PATCH 86/98] skip flaky suites (#116064) --- .../apps/observability/alerts/add_to_case.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts b/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts index 5e80a5769b44d..67dbf2368c044 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts @@ -25,7 +25,8 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); }); - describe('When user has all priviledges for cases', () => { + // FLAKY: https://github.com/elastic/kibana/issues/116064 + describe.skip('When user has all priviledges for cases', () => { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ From 36bab568f4d0a53a0bbe59ae726d675229923f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 10 Nov 2021 08:51:06 -0500 Subject: [PATCH 87/98] [APM] Search term with certain characters will cause APM UI to crash (#118063) --- .../header_filters/generate_data.ts | 47 ++++++++++++++++ .../header_filters/header_filters.spec.ts | 54 +++++++++++++++++++ .../url_params_context/resolve_url_params.ts | 2 +- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts new file mode 100644 index 0000000000000..9ebaa1747d909 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { service, timerange } from '@elastic/apm-synthtrace'; + +export function generateData({ + from, + to, + specialServiceName, +}: { + from: number; + to: number; + specialServiceName: string; +}) { + const range = timerange(from, to); + + const service1 = service(specialServiceName, 'production', 'java') + .instance('service-1-prod-1') + .podId('service-1-prod-1-pod'); + + const opbeansNode = service('opbeans-node', 'production', 'nodejs').instance( + 'opbeans-node-prod-1' + ); + + return [ + ...range + .interval('2m') + .rate(1) + .flatMap((timestamp, index) => [ + ...service1 + .transaction('GET /apple 🍎 ') + .timestamp(timestamp) + .duration(1000) + .success() + .serialize(), + ...opbeansNode + .transaction('GET /banana 🍌') + .timestamp(timestamp) + .duration(500) + .success() + .serialize(), + ]), + ]; +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts new file mode 100644 index 0000000000000..2fa8b1588a630 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts @@ -0,0 +1,54 @@ +/* + * 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 url from 'url'; +import { synthtrace } from '../../../../../synthtrace'; +import { generateData } from './generate_data'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services', + query: { rangeFrom: start, rangeTo: end }, +}); + +const specialServiceName = + 'service 1 / ? # [ ] @ ! $ & ( ) * + , ; = < > % {} | ^ ` <>'; + +describe('Service inventory - header filters', () => { + before(async () => { + await synthtrace.index( + generateData({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + specialServiceName, + }) + ); + }); + + after(async () => { + await synthtrace.clean(); + }); + + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + + describe('Filtering by kuerybar', () => { + it('filters by service.name with special characters', () => { + cy.visit(serviceOverviewHref); + cy.contains('Services'); + cy.contains('opbeans-node'); + cy.contains('service 1'); + cy.get('[data-test-subj="headerFilterKuerybar"]') + .type(`service.name: "${specialServiceName}"`) + .type('{enter}'); + cy.contains('service 1'); + cy.url().should('include', encodeURIComponent(specialServiceName)); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index 845fdb175bb65..c37d83983a00b 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -81,7 +81,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { detailTab: toString(detailTab), flyoutDetailTab: toString(flyoutDetailTab), spanId: toNumber(spanId), - kuery: kuery && decodeURIComponent(kuery), + kuery, transactionName, transactionType, searchTerm: toString(searchTerm), From 7b6ac0e65894ed4853efa9a380873d03c8c389ee Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Wed, 10 Nov 2021 15:12:23 +0100 Subject: [PATCH 88/98] [Lens] Reference lines: fix default value for Interval axis (#117847) * :bug: Fix the xAccessor computation * :white_check_mark: Add test for fix * :ok_hand: Integrated feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../reference_line_helpers.test.ts | 28 +++++++++++++++++++ .../reference_line_helpers.tsx | 21 ++++++++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts index 9dacc12c68d65..42caca7fa2e09 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts @@ -459,6 +459,34 @@ describe('reference_line helpers', () => { ).toEqual({ min: 0, max: 375 }); }); + it('should compute the correct value for a histogram on stacked chart for the xAccessor', () => { + for (const seriesType of ['bar_stacked', 'bar_horizontal_stacked', 'area_stacked']) + expect( + computeOverallDataDomain( + [ + { layerId: 'id-a', seriesType, accessors: ['c'] }, + { layerId: 'id-b', seriesType, accessors: ['f'] }, + ] as XYLayerConfig[], + ['c', 'f'], + getActiveData([ + { + id: 'id-a', + rows: Array(3) + .fill(1) + .map((_, i) => ({ a: 50 * i, b: 100 * i, c: i })), + }, + { + id: 'id-b', + rows: Array(3) + .fill(1) + .map((_, i) => ({ d: 25 * (i + 1), e: i % 2 ? 100 : null, f: i })), + }, + ]), + false // this will avoid the stacking behaviour + ) + ).toEqual({ min: 0, max: 2 }); + }); + it('should compute the correct value for a histogram non-stacked chart', () => { for (const seriesType of ['bar', 'bar_horizontal', 'line', 'area']) expect( diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index 71ce2d0ea2082..53a2d4bcc7222 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -109,7 +109,8 @@ export function getStaticValue( filteredLayers, accessors, activeData, - groupId !== 'x' // histogram axis should compute the min based on the current data + groupId !== 'x', // histogram axis should compute the min based on the current data + groupId !== 'x' ) || fallbackValue ); } @@ -152,13 +153,15 @@ function getAccessorCriteriaForGroup( export function computeOverallDataDomain( dataLayers: Array>, accessorIds: string[], - activeData: NonNullable + activeData: NonNullable, + allowStacking: boolean = true ) { const accessorMap = new Set(accessorIds); let min: number | undefined; let max: number | undefined; - const [stacked, unstacked] = partition(dataLayers, ({ seriesType }) => - isStackedChart(seriesType) + const [stacked, unstacked] = partition( + dataLayers, + ({ seriesType }) => isStackedChart(seriesType) && allowStacking ); for (const { layerId, accessors } of unstacked) { const table = activeData[layerId]; @@ -215,7 +218,8 @@ function computeStaticValueForGroup( dataLayers: Array>, accessorIds: string[], activeData: NonNullable, - minZeroOrNegativeBase: boolean = true + minZeroOrNegativeBase: boolean = true, + allowStacking: boolean = true ) { const defaultReferenceLineFactor = 3 / 4; @@ -224,7 +228,12 @@ function computeStaticValueForGroup( return defaultReferenceLineFactor; } - const { min, max } = computeOverallDataDomain(dataLayers, accessorIds, activeData); + const { min, max } = computeOverallDataDomain( + dataLayers, + accessorIds, + activeData, + allowStacking + ); if (min != null && max != null && isFinite(min) && isFinite(max)) { // Custom axis bounds can go below 0, so consider also lower values than 0 From 0b5a434d19f36b92ecdf3cc62a8d54e82e907141 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 10 Nov 2021 09:25:31 -0500 Subject: [PATCH 89/98] [Fleet] Add support for default_monitoring output (#117667) --- .../plugins/fleet/common/constants/output.ts | 1 + .../fleet/common/types/models/output.ts | 1 + .../fleet/server/saved_objects/index.ts | 3 + .../saved_objects/migrations/to_v8_0_0.ts | 22 + .../agent_policies/full_agent_policy.test.ts | 6 +- .../agent_policies/full_agent_policy.ts | 12 +- .../server/services/agent_policy.test.ts | 4 +- .../fleet/server/services/agent_policy.ts | 2 +- .../fleet/server/services/output.test.ts | 429 ++++++++++++++++-- .../plugins/fleet/server/services/output.ts | 149 ++++-- .../fleet/server/services/package_policy.ts | 2 +- .../server/services/preconfiguration.test.ts | 67 +-- .../fleet/server/services/preconfiguration.ts | 17 +- .../types/models/preconfiguration.test.ts | 40 +- .../server/types/models/preconfiguration.ts | 109 ++--- 15 files changed, 651 insertions(+), 213 deletions(-) create mode 100644 x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_0_0.ts diff --git a/x-pack/plugins/fleet/common/constants/output.ts b/x-pack/plugins/fleet/common/constants/output.ts index 9a236001aca25..c750be12be2df 100644 --- a/x-pack/plugins/fleet/common/constants/output.ts +++ b/x-pack/plugins/fleet/common/constants/output.ts @@ -18,6 +18,7 @@ export const DEFAULT_OUTPUT_ID = 'default'; export const DEFAULT_OUTPUT: NewOutput = { name: DEFAULT_OUTPUT_ID, is_default: true, + is_default_monitoring: true, type: outputType.Elasticsearch, hosts: [''], }; diff --git a/x-pack/plugins/fleet/common/types/models/output.ts b/x-pack/plugins/fleet/common/types/models/output.ts index 4f70460e89ff8..fada8171b91fc 100644 --- a/x-pack/plugins/fleet/common/types/models/output.ts +++ b/x-pack/plugins/fleet/common/types/models/output.ts @@ -12,6 +12,7 @@ export type OutputType = typeof outputType; export interface NewOutput { is_default: boolean; + is_default_monitoring: boolean; name: string; type: ValueOf; hosts?: string[]; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index e8fda952f17e6..19998c8d8bdbb 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -45,6 +45,7 @@ import { import { migratePackagePolicyToV7140, migrateInstallationToV7140 } from './migrations/to_v7_14_0'; import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0'; import { migrateInstallationToV7160, migratePackagePolicyToV7160 } from './migrations/to_v7_16_0'; +import { migrateOutputToV800 } from './migrations/to_v8_0_0'; /* * Saved object types and mappings @@ -203,6 +204,7 @@ const getSavedObjectTypes = ( name: { type: 'keyword' }, type: { type: 'keyword' }, is_default: { type: 'boolean' }, + is_default_monitoring: { type: 'boolean' }, hosts: { type: 'keyword' }, ca_sha256: { type: 'keyword', index: false }, config: { type: 'flattened' }, @@ -212,6 +214,7 @@ const getSavedObjectTypes = ( }, migrations: { '7.13.0': migrateOutputToV7130, + '8.0.0': migrateOutputToV800, }, }, [PACKAGE_POLICY_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_0_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_0_0.ts new file mode 100644 index 0000000000000..77797b3d27ba5 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_0_0.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 { SavedObjectMigrationFn } from 'kibana/server'; + +import type { Output } from '../../../common'; +import {} from '../../../common'; + +export const migrateOutputToV800: SavedObjectMigrationFn = ( + outputDoc, + migrationContext +) => { + if (outputDoc.attributes.is_default) { + outputDoc.attributes.is_default_monitoring = true; + } + + return outputDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts index 9a9b200d14130..d720aa72e18f8 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts @@ -51,13 +51,15 @@ jest.mock('../agent_policy'); jest.mock('../output', () => { return { outputService: { - getDefaultOutputId: () => 'test-id', + getDefaultDataOutputId: async () => 'test-id', + getDefaultMonitoringOutputId: async () => 'test-id', get: (soClient: any, id: string): Output => { switch (id) { case 'data-output-id': return { id: 'data-output-id', is_default: false, + is_default_monitoring: false, name: 'Data output', // @ts-ignore type: 'elasticsearch', @@ -67,6 +69,7 @@ jest.mock('../output', () => { return { id: 'monitoring-output-id', is_default: false, + is_default_monitoring: false, name: 'Monitoring output', // @ts-ignore type: 'elasticsearch', @@ -76,6 +79,7 @@ jest.mock('../output', () => { return { id: 'test-id', is_default: true, + is_default_monitoring: true, name: 'default', // @ts-ignore type: 'elasticsearch', diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index 60cf9c8d96257..f89a186c1a5f9 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -48,13 +48,17 @@ export async function getFullAgentPolicy( return null; } - const defaultOutputId = await outputService.getDefaultOutputId(soClient); - if (!defaultOutputId) { + const defaultDataOutputId = await outputService.getDefaultDataOutputId(soClient); + + if (!defaultDataOutputId) { throw new Error('Default output is not setup'); } - const dataOutputId = agentPolicy.data_output_id || defaultOutputId; - const monitoringOutputId = agentPolicy.monitoring_output_id || defaultOutputId; + const dataOutputId: string = agentPolicy.data_output_id || defaultDataOutputId; + const monitoringOutputId: string = + agentPolicy.monitoring_output_id || + (await outputService.getDefaultMonitoringOutputId(soClient)) || + dataOutputId; const outputs = await Promise.all( Array.from(new Set([dataOutputId, monitoringOutputId])).map((outputId) => diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 5617f8ef7bd7c..e28e2610b4b45 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -235,7 +235,7 @@ describe('agent policy', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); - mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedOutputService.getDefaultDataOutputId.mockResolvedValue('default-output'); mockedGetFullAgentPolicy.mockResolvedValue(null); soClient.get.mockResolvedValue({ @@ -253,7 +253,7 @@ describe('agent policy', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); - mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedOutputService.getDefaultDataOutputId.mockResolvedValue('default-output'); mockedGetFullAgentPolicy.mockResolvedValue({ id: 'policy123', revision: 1, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 7de907b9a15fa..b1a45b5a92421 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -672,7 +672,7 @@ class AgentPolicyService { ) { // Use internal ES client so we have permissions to write to .fleet* indices const esClient = appContextService.getInternalUserESClient(); - const defaultOutputId = await outputService.getDefaultOutputId(soClient); + const defaultOutputId = await outputService.getDefaultDataOutputId(soClient); if (!defaultOutputId) { return; diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts index 8103794fb0805..23ee77e0f28c2 100644 --- a/x-pack/plugins/fleet/server/services/output.test.ts +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -36,23 +36,109 @@ const CONFIG_WITHOUT_ES_HOSTS = { }, }; -function getMockedSoClient() { +function mockOutputSO(id: string, attributes: any = {}) { + return { + id: outputIdToUuid(id), + type: 'ingest-outputs', + references: [], + attributes: { + output_id: id, + ...attributes, + }, + }; +} + +function getMockedSoClient( + options: { defaultOutputId?: string; defaultOutputMonitoringId?: string } = {} +) { const soClient = savedObjectsClientMock.create(); + soClient.get.mockImplementation(async (type: string, id: string) => { switch (id) { case outputIdToUuid('output-test'): { - return { - id: outputIdToUuid('output-test'), - type: 'ingest-outputs', - references: [], - attributes: { - output_id: 'output-test', - }, - }; + return mockOutputSO('output-test'); + } + case outputIdToUuid('existing-default-output'): { + return mockOutputSO('existing-default-output'); } + case outputIdToUuid('existing-default-monitoring-output'): { + return mockOutputSO('existing-default-monitoring-output', { is_default: true }); + } + case outputIdToUuid('existing-preconfigured-default-output'): { + return mockOutputSO('existing-preconfigured-default-output', { + is_default: true, + is_preconfigured: true, + }); + } + default: - throw new Error('not found'); + throw new Error('not found: ' + id); + } + }); + soClient.update.mockImplementation(async (type, id, data) => { + return { + id, + type, + attributes: {}, + references: [], + }; + }); + soClient.create.mockImplementation(async (type, data, createOptions) => { + return { + id: createOptions?.id || 'generated-id', + type, + attributes: {}, + references: [], + }; + }); + soClient.find.mockImplementation(async (findOptions) => { + if ( + options?.defaultOutputMonitoringId && + findOptions.searchFields && + findOptions.searchFields.includes('is_default_monitoring') && + findOptions.search === 'true' + ) { + return { + page: 1, + per_page: 10, + saved_objects: [ + { + score: 0, + ...(await soClient.get( + 'ingest-outputs', + outputIdToUuid(options.defaultOutputMonitoringId) + )), + }, + ], + total: 1, + }; + } + + if ( + options?.defaultOutputId && + findOptions.searchFields && + findOptions.searchFields.includes('is_default') && + findOptions.search === 'true' + ) { + return { + page: 1, + per_page: 10, + saved_objects: [ + { + score: 0, + ...(await soClient.get('ingest-outputs', outputIdToUuid(options.defaultOutputId))), + }, + ], + total: 1, + }; } + + return { + page: 1, + per_page: 10, + saved_objects: [], + total: 0, + }; }); return soClient; @@ -62,16 +148,12 @@ describe('Output Service', () => { describe('create', () => { it('work with a predefined id', async () => { const soClient = getMockedSoClient(); - soClient.create.mockResolvedValue({ - id: outputIdToUuid('output-test'), - type: 'ingest-output', - attributes: {}, - references: [], - }); + await outputService.create( soClient, { is_default: false, + is_default_monitoring: false, name: 'Test', type: 'elasticsearch', }, @@ -86,6 +168,285 @@ describe('Output Service', () => { 'output-test' ); }); + + it('should create a new default output if none exists before', async () => { + const soClient = getMockedSoClient(); + + await outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.update).not.toBeCalled(); + }); + + it('should update existing default output when creating a new default output', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + await outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.update).toBeCalledTimes(1); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-output'), + { is_default: false } + ); + }); + + it('should create a new default monitoring output if none exists before', async () => { + const soClient = getMockedSoClient(); + + await outputService.create( + soClient, + { + is_default: false, + is_default_monitoring: true, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.update).not.toBeCalled(); + }); + + it('should update existing default monitoring output when creating a new default output', async () => { + const soClient = getMockedSoClient({ + defaultOutputMonitoringId: 'existing-default-monitoring-output', + }); + + await outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: true, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ); + + expect(soClient.update).toBeCalledTimes(1); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-monitoring-output'), + { is_default_monitoring: false } + ); + }); + + // With preconfigured outputs + it('should throw when an existing preconfigured default output and creating a new default output outside of preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-preconfigured-default-output', + }); + + await expect( + outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test' } + ) + ).rejects.toThrow( + `Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.` + ); + }); + + it('should update existing default preconfigured monitoring output when creating a new default output from preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-preconfigured-default-output', + }); + + await outputService.create( + soClient, + { + is_default: true, + is_default_monitoring: true, + name: 'Test', + type: 'elasticsearch', + }, + { id: 'output-test', fromPreconfiguration: true } + ); + + expect(soClient.update).toBeCalledTimes(1); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-preconfigured-default-output'), + { is_default: false } + ); + }); + }); + + describe('update', () => { + it('should update existing default output when updating an output to become the default output', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + await outputService.update(soClient, 'output-test', { + is_default: true, + }); + + expect(soClient.update).toBeCalledTimes(2); + expect(soClient.update).toBeCalledWith(expect.anything(), outputIdToUuid('output-test'), { + is_default: true, + }); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-output'), + { is_default: false } + ); + }); + + it('should not update existing default output when the output is already the default one', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + await outputService.update(soClient, 'existing-default-output', { + is_default: true, + name: 'Test', + }); + + expect(soClient.update).toBeCalledTimes(1); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-output'), + { is_default: true, name: 'Test' } + ); + }); + + it('should update existing default monitoring output when updating an output to become the default monitoring output', async () => { + const soClient = getMockedSoClient({ + defaultOutputMonitoringId: 'existing-default-monitoring-output', + }); + + await outputService.update(soClient, 'output-test', { + is_default_monitoring: true, + }); + + expect(soClient.update).toBeCalledTimes(2); + expect(soClient.update).toBeCalledWith(expect.anything(), outputIdToUuid('output-test'), { + is_default_monitoring: true, + }); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-monitoring-output'), + { is_default_monitoring: false } + ); + }); + + // With preconfigured outputs + it('Do not allow to update a preconfigured output outisde from preconfiguration', async () => { + const soClient = getMockedSoClient(); + await expect( + outputService.update(soClient, 'existing-preconfigured-default-output', { + config_yaml: '', + }) + ).rejects.toThrow( + 'Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.' + ); + }); + + it('Allow to update a preconfigured output from preconfiguration', async () => { + const soClient = getMockedSoClient(); + await outputService.update( + soClient, + 'existing-preconfigured-default-output', + { + config_yaml: '', + }, + { + fromPreconfiguration: true, + } + ); + + expect(soClient.update).toBeCalled(); + }); + + it('Should throw when an existing preconfigured default output and updating an output to become the default one outside of preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-preconfigured-default-output', + }); + + await expect( + outputService.update(soClient, 'output-test', { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }) + ).rejects.toThrow( + `Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.` + ); + }); + + it('Should update existing default preconfigured monitoring output when updating an output to become the default one from preconfiguration', async () => { + const soClient = getMockedSoClient({ + defaultOutputId: 'existing-default-output', + }); + + await outputService.update( + soClient, + 'output-test', + { + is_default: true, + is_default_monitoring: false, + name: 'Test', + type: 'elasticsearch', + }, + { fromPreconfiguration: true } + ); + + expect(soClient.update).toBeCalledTimes(2); + expect(soClient.update).toBeCalledWith( + expect.anything(), + outputIdToUuid('existing-default-output'), + { is_default: false } + ); + }); + }); + + describe('delete', () => { + // Preconfigured output + it('Do not allow to delete a preconfigured output outisde from preconfiguration', async () => { + const soClient = getMockedSoClient(); + await expect( + outputService.delete(soClient, 'existing-preconfigured-default-output') + ).rejects.toThrow( + 'Preconfigured output existing-preconfigured-default-output cannot be deleted outside of kibana config file.' + ); + }); + + it('Allow to delete a preconfigured output from preconfiguration', async () => { + const soClient = getMockedSoClient(); + await outputService.delete(soClient, 'existing-preconfigured-default-output', { + fromPreconfiguration: true, + }); + + expect(soClient.delete).toBeCalled(); + }); }); describe('get', () => { @@ -99,27 +460,25 @@ describe('Output Service', () => { }); }); - describe('getDefaultOutputId', () => { + describe('getDefaultDataOutputId', () => { it('work with a predefined id', async () => { - const soClient = getMockedSoClient(); - soClient.find.mockResolvedValue({ - page: 1, - per_page: 100, - total: 1, - saved_objects: [ - { - id: outputIdToUuid('output-test'), - type: 'ingest-outputs', - references: [], - score: 0, - attributes: { - output_id: 'output-test', - is_default: true, - }, - }, - ], + const soClient = getMockedSoClient({ + defaultOutputId: 'output-test', + }); + const defaultId = await outputService.getDefaultDataOutputId(soClient); + + expect(soClient.find).toHaveBeenCalled(); + + expect(defaultId).toEqual('output-test'); + }); + }); + + describe('getDefaultMonitoringOutputOd', () => { + it('work with a predefined id', async () => { + const soClient = getMockedSoClient({ + defaultOutputMonitoringId: 'output-test', }); - const defaultId = await outputService.getDefaultOutputId(soClient); + const defaultId = await outputService.getDefaultMonitoringOutputId(soClient); expect(soClient.find).toHaveBeenCalled(); diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 5a7ba1e2c1223..e39f70671a232 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -10,7 +10,7 @@ import uuid from 'uuid/v5'; import type { NewOutput, Output, OutputSOAttributes } from '../types'; import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; -import { decodeCloudId, normalizeHostsForAgents } from '../../common'; +import { decodeCloudId, normalizeHostsForAgents, SO_SEARCH_LIMIT } from '../../common'; import { appContextService } from './app_context'; @@ -44,7 +44,7 @@ function outputSavedObjectToOutput(so: SavedObject) { } class OutputService { - private async _getDefaultOutputsSO(soClient: SavedObjectsClientContract) { + private async _getDefaultDataOutputsSO(soClient: SavedObjectsClientContract) { return await soClient.find({ type: OUTPUT_SAVED_OBJECT_TYPE, searchFields: ['is_default'], @@ -52,20 +52,32 @@ class OutputService { }); } + private async _getDefaultMonitoringOutputsSO(soClient: SavedObjectsClientContract) { + return await soClient.find({ + type: OUTPUT_SAVED_OBJECT_TYPE, + searchFields: ['is_default_monitoring'], + search: 'true', + }); + } + public async ensureDefaultOutput(soClient: SavedObjectsClientContract) { - const outputs = await this._getDefaultOutputsSO(soClient); + const outputs = await this.list(soClient); - if (!outputs.saved_objects.length) { + const defaultOutput = outputs.items.find((o) => o.is_default); + const defaultMonitoringOutput = outputs.items.find((o) => o.is_default_monitoring); + + if (!defaultOutput) { const newDefaultOutput = { ...DEFAULT_OUTPUT, hosts: this.getDefaultESHosts(), ca_sha256: appContextService.getConfig()!.agents.elasticsearch.ca_sha256, + is_default_monitoring: !defaultMonitoringOutput, } as NewOutput; return await this.create(soClient, newDefaultOutput); } - return outputSavedObjectToOutput(outputs.saved_objects[0]); + return defaultOutput; } public getDefaultESHosts(): string[] { @@ -82,8 +94,18 @@ class OutputService { return cloudHosts || flagHosts || DEFAULT_ES_HOSTS; } - public async getDefaultOutputId(soClient: SavedObjectsClientContract) { - const outputs = await this._getDefaultOutputsSO(soClient); + public async getDefaultDataOutputId(soClient: SavedObjectsClientContract) { + const outputs = await this._getDefaultDataOutputsSO(soClient); + + if (!outputs.saved_objects.length) { + return null; + } + + return outputSavedObjectToOutput(outputs.saved_objects[0]).id; + } + + public async getDefaultMonitoringOutputId(soClient: SavedObjectsClientContract) { + const outputs = await this._getDefaultMonitoringOutputsSO(soClient); if (!outputs.saved_objects.length) { return null; @@ -95,15 +117,31 @@ class OutputService { public async create( soClient: SavedObjectsClientContract, output: NewOutput, - options?: { id?: string; overwrite?: boolean } + options?: { id?: string; fromPreconfiguration?: boolean } ): Promise { const data: OutputSOAttributes = { ...output }; // ensure only default output exists if (data.is_default) { - const defaultOuput = await this.getDefaultOutputId(soClient); - if (defaultOuput) { - throw new Error(`A default output already exists (${defaultOuput})`); + const defaultDataOuputId = await this.getDefaultDataOutputId(soClient); + if (defaultDataOuputId) { + await this.update( + soClient, + defaultDataOuputId, + { is_default: false }, + { fromPreconfiguration: options?.fromPreconfiguration ?? false } + ); + } + } + if (data.is_default_monitoring) { + const defaultMonitoringOutputId = await this.getDefaultMonitoringOutputId(soClient); + if (defaultMonitoringOutputId) { + await this.update( + soClient, + defaultMonitoringOutputId, + { is_default_monitoring: false }, + { fromPreconfiguration: options?.fromPreconfiguration ?? false } + ); } } @@ -116,7 +154,7 @@ class OutputService { } const newSo = await soClient.create(SAVED_OBJECT_TYPE, data, { - ...options, + overwrite: options?.fromPreconfiguration, id: options?.id ? outputIdToUuid(options.id) : undefined, }); @@ -149,6 +187,21 @@ class OutputService { .filter((output): output is Output => typeof output !== 'undefined'); } + public async list(soClient: SavedObjectsClientContract) { + const outputs = await soClient.find({ + type: SAVED_OBJECT_TYPE, + page: 1, + perPage: SO_SEARCH_LIMIT, + }); + + return { + items: outputs.saved_objects.map(outputSavedObjectToOutput), + total: outputs.total, + page: outputs.page, + perPage: outputs.per_page, + }; + } + public async get(soClient: SavedObjectsClientContract, id: string): Promise { const outputSO = await soClient.get(SAVED_OBJECT_TYPE, outputIdToUuid(id)); @@ -159,13 +212,66 @@ class OutputService { return outputSavedObjectToOutput(outputSO); } - public async delete(soClient: SavedObjectsClientContract, id: string) { + public async delete( + soClient: SavedObjectsClientContract, + id: string, + { fromPreconfiguration = false }: { fromPreconfiguration?: boolean } = { + fromPreconfiguration: false, + } + ) { + const originalOutput = await this.get(soClient, id); + + if (originalOutput.is_preconfigured && !fromPreconfiguration) { + throw new Error( + `Preconfigured output ${id} cannot be deleted outside of kibana config file.` + ); + } return soClient.delete(SAVED_OBJECT_TYPE, outputIdToUuid(id)); } - public async update(soClient: SavedObjectsClientContract, id: string, data: Partial) { + public async update( + soClient: SavedObjectsClientContract, + id: string, + data: Partial, + { fromPreconfiguration = false }: { fromPreconfiguration: boolean } = { + fromPreconfiguration: false, + } + ) { + const originalOutput = await this.get(soClient, id); + + if (originalOutput.is_preconfigured && !fromPreconfiguration) { + throw new Error( + `Preconfigured output ${id} cannot be updated outside of kibana config file.` + ); + } + const updateData = { ...data }; + // ensure only default output exists + if (data.is_default) { + const defaultDataOuputId = await this.getDefaultDataOutputId(soClient); + if (defaultDataOuputId && defaultDataOuputId !== id) { + await this.update( + soClient, + defaultDataOuputId, + { is_default: false }, + { fromPreconfiguration } + ); + } + } + if (data.is_default_monitoring) { + const defaultMonitoringOutputId = await this.getDefaultMonitoringOutputId(soClient); + + if (defaultMonitoringOutputId && defaultMonitoringOutputId !== id) { + await this.update( + soClient, + defaultMonitoringOutputId, + { is_default_monitoring: false }, + { fromPreconfiguration } + ); + } + } + if (updateData.hosts) { updateData.hosts = updateData.hosts.map(normalizeHostsForAgents); } @@ -179,21 +285,6 @@ class OutputService { throw new Error(outputSO.error.message); } } - - public async list(soClient: SavedObjectsClientContract) { - const outputs = await soClient.find({ - type: SAVED_OBJECT_TYPE, - page: 1, - perPage: 1000, - }); - - return { - items: outputs.saved_objects.map(outputSavedObjectToOutput), - total: outputs.total, - page: 1, - perPage: 1000, - }; - } } export const outputService = new OutputService(); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index c4ef15f4e7ed9..af5596964740a 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -716,7 +716,7 @@ class PackagePolicyService { pkgName: pkgInstall.name, pkgVersion: pkgInstall.version, }), - outputService.getDefaultOutputId(soClient), + outputService.getDefaultDataOutputId(soClient), ]); if (packageInfo) { if (!defaultOutputId) { diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 2899c327e8d2b..6fefc4631239d 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -40,6 +40,7 @@ const mockConfiguredPolicies = new Map(); const mockDefaultOutput: Output = { id: 'test-id', is_default: true, + is_default_monitoring: false, name: 'default', // @ts-ignore type: 'elasticsearch', @@ -547,17 +548,6 @@ describe('comparePreconfiguredPolicyToCurrent', () => { ); expect(hasChanged).toBe(false); }); - - it('should not return hasChanged when only namespace field changes', () => { - const { hasChanged } = comparePreconfiguredPolicyToCurrent( - { - ...baseConfig, - namespace: 'newnamespace', - }, - basePackagePolicy - ); - expect(hasChanged).toBe(false); - }); }); describe('output preconfiguration', () => { @@ -565,13 +555,14 @@ describe('output preconfiguration', () => { mockedOutputService.create.mockReset(); mockedOutputService.update.mockReset(); mockedOutputService.delete.mockReset(); - mockedOutputService.getDefaultOutputId.mockReset(); + mockedOutputService.getDefaultDataOutputId.mockReset(); mockedOutputService.getDefaultESHosts.mockReturnValue(['http://default-es:9200']); mockedOutputService.bulkGet.mockImplementation(async (soClient, id): Promise => { return [ { id: 'existing-output-1', is_default: false, + is_default_monitoring: false, name: 'Output 1', // @ts-ignore type: 'elasticsearch', @@ -591,6 +582,7 @@ describe('output preconfiguration', () => { name: 'Output 1', type: 'elasticsearch', is_default: false, + is_default_monitoring: false, hosts: ['http://test.fr'], }, ]); @@ -600,26 +592,6 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); }); - it('should delete existing default output if a new preconfigured output is added', async () => { - const soClient = savedObjectsClientMock.create(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output-123'); - await ensurePreconfiguredOutputs(soClient, esClient, [ - { - id: 'non-existing-default-output-1', - name: 'Output 1', - type: 'elasticsearch', - is_default: true, - hosts: ['http://test.fr'], - }, - ]); - - expect(mockedOutputService.delete).toBeCalled(); - expect(mockedOutputService.create).toBeCalled(); - expect(mockedOutputService.update).not.toBeCalled(); - expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); - }); - it('should set default hosts if hosts is not set output that does not exists', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -629,6 +601,7 @@ describe('output preconfiguration', () => { name: 'Output 1', type: 'elasticsearch', is_default: false, + is_default_monitoring: false, }, ]); @@ -644,6 +617,7 @@ describe('output preconfiguration', () => { { id: 'existing-output-1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://newhostichanged.co:9201'], // field that changed @@ -655,36 +629,16 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); }); - it('should delete default output if preconfigured output exists and another default output exists', async () => { - const soClient = savedObjectsClientMock.create(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - soClient.find.mockResolvedValue({ saved_objects: [], page: 0, per_page: 0, total: 0 }); - mockedOutputService.getDefaultOutputId.mockResolvedValue('default-123'); - await ensurePreconfiguredOutputs(soClient, esClient, [ - { - id: 'existing-output-1', - is_default: true, - name: 'Output 1', - type: 'elasticsearch', - hosts: ['http://newhostichanged.co:9201'], // field that changed - }, - ]); - - expect(mockedOutputService.delete).toBeCalled(); - expect(mockedOutputService.create).not.toBeCalled(); - expect(mockedOutputService.update).toBeCalled(); - expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); - }); - it('should not delete default output if preconfigured default output exists and changed', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; soClient.find.mockResolvedValue({ saved_objects: [], page: 0, per_page: 0, total: 0 }); - mockedOutputService.getDefaultOutputId.mockResolvedValue('existing-output-1'); + mockedOutputService.getDefaultDataOutputId.mockResolvedValue('existing-output-1'); await ensurePreconfiguredOutputs(soClient, esClient, [ { id: 'existing-output-1', is_default: true, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://newhostichanged.co:9201'], // field that changed @@ -703,6 +657,7 @@ describe('output preconfiguration', () => { data: { id: 'existing-output-1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://es.co:80'], @@ -713,6 +668,7 @@ describe('output preconfiguration', () => { data: { id: 'existing-output-1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://es.co'], @@ -746,6 +702,7 @@ describe('output preconfiguration', () => { { id: 'output1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://es.co:9201'], @@ -753,6 +710,7 @@ describe('output preconfiguration', () => { { id: 'output2', is_default: false, + is_default_monitoring: false, name: 'Output 2', type: 'elasticsearch', hosts: ['http://es.co:9201'], @@ -777,6 +735,7 @@ describe('output preconfiguration', () => { { id: 'output1', is_default: false, + is_default_monitoring: false, name: 'Output 1', type: 'elasticsearch', hosts: ['http://es.co:9201'], diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index e5fea73815ea7..6cdb3abf24908 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -55,6 +55,7 @@ function isPreconfiguredOutputDifferentFromCurrent( ): boolean { return ( existingOutput.is_default !== preconfiguredOutput.is_default || + existingOutput.is_default_monitoring !== preconfiguredOutput.is_default_monitoring || existingOutput.name !== preconfiguredOutput.name || existingOutput.type !== preconfiguredOutput.type || (preconfiguredOutput.hosts && @@ -103,21 +104,13 @@ export async function ensurePreconfiguredOutputs( const isCreate = !existingOutput; const isUpdateWithNewData = existingOutput && isPreconfiguredOutputDifferentFromCurrent(existingOutput, data); - // If a default output already exists, delete it in favor of the preconfigured one - if (isCreate || isUpdateWithNewData) { - const defaultOutputId = await outputService.getDefaultOutputId(soClient); - - if (defaultOutputId && defaultOutputId !== output.id) { - await outputService.delete(soClient, defaultOutputId); - } - } if (isCreate) { - await outputService.create(soClient, data, { id, overwrite: true }); + await outputService.create(soClient, data, { id, fromPreconfiguration: true }); } else if (isUpdateWithNewData) { - await outputService.update(soClient, id, data); + await outputService.update(soClient, id, data, { fromPreconfiguration: true }); // Bump revision of all policies using that output - if (outputData.is_default) { + if (outputData.is_default || outputData.is_default_monitoring) { await agentPolicyService.bumpAllAgentPolicies(soClient, esClient); } else { await agentPolicyService.bumpAllAgentPoliciesForOutput(soClient, esClient, id); @@ -139,7 +132,7 @@ export async function cleanPreconfiguredOutputs( for (const output of existingPreconfiguredOutput) { if (!outputs.find(({ id }) => output.id === id)) { logger.info(`Deleting preconfigured output ${output.id}`); - await outputService.delete(soClient, output.id); + await outputService.delete(soClient, output.id, { fromPreconfiguration: true }); } } } diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts index eb349e0d0f823..9cf8626f5fed5 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PreconfiguredOutputsSchema, PreconfiguredAgentPoliciesSchema } from './preconfiguration'; +import { PreconfiguredOutputsSchema } from './preconfiguration'; describe('Test preconfiguration schema', () => { describe('PreconfiguredOutputsSchema', () => { @@ -25,7 +25,25 @@ describe('Test preconfiguration schema', () => { is_default: true, }, ]); - }).toThrowError('preconfigured outputs need to have only one default output.'); + }).toThrowError('preconfigured outputs can only have one default output.'); + }); + it('should not allow multiple default monitoring output', () => { + expect(() => { + PreconfiguredOutputsSchema.validate([ + { + id: 'output-1', + name: 'Output 1', + type: 'elasticsearch', + is_default_monitoring: true, + }, + { + id: 'output-2', + name: 'Output 2', + type: 'elasticsearch', + is_default_monitoring: true, + }, + ]); + }).toThrowError('preconfigured outputs can only have one default monitoring output.'); }); it('should not allow multiple output with same ids', () => { expect(() => { @@ -60,22 +78,4 @@ describe('Test preconfiguration schema', () => { }).toThrowError('preconfigured outputs need to have unique names.'); }); }); - - describe('PreconfiguredAgentPoliciesSchema', () => { - it('should not allow multiple outputs in one policy', () => { - expect(() => { - PreconfiguredAgentPoliciesSchema.validate([ - { - id: 'policy-1', - name: 'Policy 1', - package_policies: [], - data_output_id: 'test1', - monitoring_output_id: 'test2', - }, - ]); - }).toThrowError( - '[0]: Currently Fleet only support one output per agent policy data_output_id should be the same as monitoring_output_id.' - ); - }); - }); }); diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index b65fa122911dc..3ba89f1e526b3 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -50,7 +50,12 @@ export const PreconfiguredPackagesSchema = schema.arrayOf( ); function validatePreconfiguredOutputs(outputs: PreconfiguredOutput[]) { - const acc = { names: new Set(), ids: new Set(), is_default: false }; + const acc = { + names: new Set(), + ids: new Set(), + is_default_exists: false, + is_default_monitoring_exists: false, + }; for (const output of outputs) { if (acc.names.has(output.name)) { @@ -59,13 +64,17 @@ function validatePreconfiguredOutputs(outputs: PreconfiguredOutput[]) { if (acc.ids.has(output.id)) { return 'preconfigured outputs need to have unique ids.'; } - if (acc.is_default && output.is_default) { - return 'preconfigured outputs need to have only one default output.'; + if (acc.is_default_exists && output.is_default) { + return 'preconfigured outputs can only have one default output.'; + } + if (acc.is_default_monitoring_exists && output.is_default_monitoring) { + return 'preconfigured outputs can only have one default monitoring output.'; } acc.ids.add(output.id); acc.names.add(output.name); - acc.is_default = acc.is_default || output.is_default; + acc.is_default_exists = acc.is_default_exists || output.is_default; + acc.is_default_monitoring_exists = acc.is_default_exists || output.is_default_monitoring; } } @@ -73,6 +82,7 @@ export const PreconfiguredOutputsSchema = schema.arrayOf( schema.object({ id: schema.string(), is_default: schema.boolean({ defaultValue: false }), + is_default_monitoring: schema.boolean({ defaultValue: false }), name: schema.string(), type: schema.oneOf([schema.literal(outputType.Elasticsearch)]), hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }))), @@ -86,57 +96,48 @@ export const PreconfiguredOutputsSchema = schema.arrayOf( ); export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( - schema.object( - { - ...AgentPolicyBaseSchema, - namespace: schema.maybe(NamespaceSchema), - id: schema.maybe(schema.oneOf([schema.string(), schema.number()])), - is_default: schema.maybe(schema.boolean()), - is_default_fleet_server: schema.maybe(schema.boolean()), - data_output_id: schema.maybe(schema.string()), - monitoring_output_id: schema.maybe(schema.string()), - package_policies: schema.arrayOf( - schema.object({ + schema.object({ + ...AgentPolicyBaseSchema, + namespace: schema.maybe(NamespaceSchema), + id: schema.maybe(schema.oneOf([schema.string(), schema.number()])), + is_default: schema.maybe(schema.boolean()), + is_default_fleet_server: schema.maybe(schema.boolean()), + data_output_id: schema.maybe(schema.string()), + monitoring_output_id: schema.maybe(schema.string()), + package_policies: schema.arrayOf( + schema.object({ + name: schema.string(), + package: schema.object({ name: schema.string(), - package: schema.object({ - name: schema.string(), - }), - description: schema.maybe(schema.string()), - namespace: schema.maybe(NamespaceSchema), - inputs: schema.maybe( - schema.arrayOf( - schema.object({ - type: schema.string(), - enabled: schema.maybe(schema.boolean()), - keep_enabled: schema.maybe(schema.boolean()), - vars: varsSchema, - streams: schema.maybe( - schema.arrayOf( - schema.object({ - data_stream: schema.object({ - type: schema.maybe(schema.string()), - dataset: schema.string(), - }), - enabled: schema.maybe(schema.boolean()), - keep_enabled: schema.maybe(schema.boolean()), - vars: varsSchema, - }) - ) - ), - }) - ) - ), - }) - ), - }, - { - validate: (policy) => { - if (policy.data_output_id !== policy.monitoring_output_id) { - return 'Currently Fleet only support one output per agent policy data_output_id should be the same as monitoring_output_id.'; - } - }, - } - ), + }), + description: schema.maybe(schema.string()), + namespace: schema.maybe(NamespaceSchema), + inputs: schema.maybe( + schema.arrayOf( + schema.object({ + type: schema.string(), + enabled: schema.maybe(schema.boolean()), + keep_enabled: schema.maybe(schema.boolean()), + vars: varsSchema, + streams: schema.maybe( + schema.arrayOf( + schema.object({ + data_stream: schema.object({ + type: schema.maybe(schema.string()), + dataset: schema.string(), + }), + enabled: schema.maybe(schema.boolean()), + keep_enabled: schema.maybe(schema.boolean()), + vars: varsSchema, + }) + ) + ), + }) + ) + ), + }) + ), + }), { defaultValue: [DEFAULT_AGENT_POLICY, DEFAULT_FLEET_SERVER_AGENT_POLICY], } From d0e30f5475f5c2628c06e4b21353470c5d79704c Mon Sep 17 00:00:00 2001 From: Jan Monschke Date: Wed, 10 Nov 2021 15:47:00 +0100 Subject: [PATCH 90/98] [Security Solution][Investigations] Fix overlapping tooltips (#117874) * feat: allow to pass `tooltipPosition` to `DefaultDraggable` * fix: prevent suricata field name and google url tooltips from overlapping * test: add test for passing the tooltipPosition * chore: distinguish between type imports and regular imports * test: update suricata signature snapshots * test: make sure that suricata signature tooltips do not overlap Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cypress/helpers/rules.ts | 15 +++++++++++++ .../timelines/row_renderers.spec.ts | 22 +++++++++++++++++++ .../cypress/screens/timeline.ts | 6 +++++ .../components/draggables/index.test.tsx | 17 ++++++++++++++ .../common/components/draggables/index.tsx | 19 ++++++++++++---- .../suricata_signature.test.tsx.snap | 1 + .../renderers/suricata/suricata_signature.tsx | 1 + 7 files changed, 77 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/helpers/rules.ts b/x-pack/plugins/security_solution/cypress/helpers/rules.ts index ebe357c382770..63542f9a78f84 100644 --- a/x-pack/plugins/security_solution/cypress/helpers/rules.ts +++ b/x-pack/plugins/security_solution/cypress/helpers/rules.ts @@ -20,3 +20,18 @@ export const formatMitreAttackDescription = (mitre: Mitre[]) => { ) .join(''); }; + +export const elementsOverlap = ($element1: JQuery, $element2: JQuery) => { + const rectA = $element1[0].getBoundingClientRect(); + const rectB = $element2[0].getBoundingClientRect(); + + // If they don't overlap horizontally, they don't overlap + if (rectA.right < rectB.left || rectB.right < rectA.left) { + return false; + } else if (rectA.bottom < rectB.top || rectB.bottom < rectA.top) { + // If they don't overlap vertically, they don't overlap + return false; + } else { + return true; + } +}; diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts index 0755142fbdc58..2219339d0577d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts @@ -5,12 +5,16 @@ * 2.0. */ +import { elementsOverlap } from '../../helpers/rules'; import { TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN, TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON, TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX, TIMELINE_ROW_RENDERERS_SEARCHBOX, TIMELINE_SHOW_ROW_RENDERERS_GEAR, + TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE, + TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE_TOOLTIP, + TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP, } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; @@ -81,4 +85,22 @@ describe('Row renderers', () => { cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200); }); + + describe('Suricata', () => { + it('Signature tooltips do not overlap', () => { + // Hover the signature to show the tooltips + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE) + .parents('.euiPopover__anchor') + .trigger('mouseover'); + + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP).then(($googleLinkTooltip) => { + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE_TOOLTIP).then(($signatureTooltip) => { + expect( + elementsOverlap($googleLinkTooltip, $signatureTooltip), + 'tooltips do not overlap' + ).to.equal(false); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index f7495f3730dc4..619e7d01f10e2 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -245,6 +245,12 @@ export const TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX = `${TIMELINE_ROW_RENDE export const TIMELINE_ROW_RENDERERS_SEARCHBOX = `${TIMELINE_ROW_RENDERERS_MODAL} input[type="search"]`; +export const TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE = `${TIMELINE_ROW_RENDERERS_MODAL} [data-test-subj="render-content-suricata.eve.alert.signature"]`; + +export const TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP = `[data-test-subj="externalLinkTooltip"]`; + +export const TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE_TOOLTIP = `[data-test-subj="suricata.eve.alert.signature-tooltip"]`; + export const TIMELINE_SHOW_ROW_RENDERERS_GEAR = '[data-test-subj="show-row-renderers-gear"]'; export const TIMELINE_TABS = '[data-test-subj="timeline"] .euiTabs'; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx index f77bf0f347f79..e1f052dbf83b0 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { EuiToolTip } from '@elastic/eui'; import { DRAGGABLE_KEYBOARD_INSTRUCTIONS_NOT_DRAGGING_SCREEN_READER_ONLY } from '../drag_and_drop/translations'; import { TestProviders } from '../../mock'; @@ -326,5 +327,21 @@ describe('draggables', () => { expect(wrapper.find('[data-test-subj="some-field-tooltip"]').first().exists()).toBe(false); }); + + test('it uses the specified tooltipPosition', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(EuiToolTip).first().props().position).toEqual('top'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx index e33a8e42e6a39..26eaec4f7a76e 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import type { IconType, ToolTipPositions } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -29,6 +30,7 @@ export interface DefaultDraggableType { children?: React.ReactNode; timelineId?: string; tooltipContent?: React.ReactNode; + tooltipPosition?: ToolTipPositions; } /** @@ -60,11 +62,13 @@ export const Content = React.memo<{ children?: React.ReactNode; field: string; tooltipContent?: React.ReactNode; + tooltipPosition?: ToolTipPositions; value?: string | null; -}>(({ children, field, tooltipContent, value }) => +}>(({ children, field, tooltipContent, tooltipPosition, value }) => !tooltipContentIsExplicitlyNull(tooltipContent) ? ( <>{children ? children : value} @@ -88,6 +92,7 @@ Content.displayName = 'Content'; * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior * @param tooltipContent - defaults to displaying `field`, pass `null` to * prevent a tooltip from being displayed, or pass arbitrary content + * @param tooltipPosition - defaults to eui's default tooltip position * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data * @param hideTopN - defaults to `false`, when true, the option to aggregate this field will be hidden */ @@ -102,6 +107,7 @@ export const DefaultDraggable = React.memo( children, timelineId, tooltipContent, + tooltipPosition, queryValue, }) => { const dataProviderProp: DataProvider = useMemo( @@ -128,11 +134,16 @@ export const DefaultDraggable = React.memo( ) : ( - + {children} ), - [children, field, tooltipContent, value] + [children, field, tooltipContent, tooltipPosition, value] ); if (value == null) return null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap index e55465cfd8895..c9f04ca2313a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap @@ -26,6 +26,7 @@ exports[`SuricataSignature rendering it renders the default SuricataSignature 1` data-test-subj="draggable-signature-link" field="suricata.eve.alert.signature" id="suricata-signature-default-draggable-test-doc-id-123-suricata.eve.alert.signature" + tooltipPosition="bottom" value="ET SCAN ATTACK Hello" >
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx index 18e2d0844779b..ea721200730e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -130,6 +130,7 @@ export const SuricataSignature = React.memo<{ id={`suricata-signature-default-draggable-${contextId}-${id}-${SURICATA_SIGNATURE_FIELD_NAME}`} isDraggable={isDraggable} value={signature} + tooltipPosition="bottom" >
From 9888b7fec3f84e76d8f32eb1757a71fa731c867d Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 10 Nov 2021 10:33:20 -0500 Subject: [PATCH 91/98] [CI] Rebalance/split cigroups and speed up overall CI time (#113676) --- .buildkite/pipelines/es_snapshots/verify.yml | 6 ++--- .buildkite/pipelines/flaky_tests/runner.js | 6 ++--- .buildkite/pipelines/hourly.yml | 12 +++++----- .buildkite/pipelines/pull_request/base.yml | 16 ++++++------- .../type_check_plugin_public_api_docs.sh | 24 ++++++++++++++++--- .ci/ci_groups.yml | 14 +++++++++++ test/functional/apps/management/index.ts | 2 +- test/functional/apps/visualize/index.ts | 4 ++-- .../security_and_spaces/tests/index.ts | 2 +- x-pack/test/api_integration/apis/index.ts | 4 ++-- .../api_integration/apis/security/index.ts | 2 +- .../test/api_integration/apis/spaces/index.ts | 2 +- .../security_and_spaces/tests/basic/index.ts | 2 +- .../security_and_spaces/tests/trial/index.ts | 3 +-- .../exception_operators_data_types/index.ts | 4 ++-- .../test/functional/apps/dashboard/index.ts | 2 +- x-pack/test/functional/apps/discover/index.ts | 2 +- x-pack/test/functional/apps/lens/index.ts | 13 ++++++---- x-pack/test/functional/apps/maps/index.js | 8 +++++-- x-pack/test/functional/apps/ml/index.ts | 21 +++++++++------- x-pack/test/functional/apps/security/index.ts | 2 +- .../test/functional/apps/transform/index.ts | 2 +- x-pack/test/functional_basic/apps/ml/index.ts | 2 +- .../security_and_spaces/apis/index.ts | 2 +- .../functional/tests/index.ts | 2 +- .../tests/kerberos/index.ts | 2 +- .../tests/saml/index.ts | 2 +- .../tests/session_idle/index.ts | 2 +- 28 files changed, 105 insertions(+), 60 deletions(-) diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 9cddade0b7482..93f8765ec16f7 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -29,7 +29,7 @@ steps: label: 'Default CI Group' parallelism: 13 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -41,7 +41,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -77,7 +77,7 @@ steps: - command: .buildkite/scripts/steps/test/api_integration.sh label: 'API Integration Tests' agents: - queue: jest + queue: n2-2 timeout_in_minutes: 120 key: api-integration diff --git a/.buildkite/pipelines/flaky_tests/runner.js b/.buildkite/pipelines/flaky_tests/runner.js index b5ccab137fd01..0c2db5c724f7b 100644 --- a/.buildkite/pipelines/flaky_tests/runner.js +++ b/.buildkite/pipelines/flaky_tests/runner.js @@ -78,7 +78,7 @@ for (const testSuite of testSuites) { steps.push({ command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, label: `Default CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-6' }, + agents: { queue: 'n2-4' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, @@ -103,7 +103,7 @@ for (const testSuite of testSuites) { steps.push({ command: `.buildkite/scripts/steps/functional/${IS_XPACK ? 'xpack' : 'oss'}_firefox.sh`, label: `${IS_XPACK ? 'Default' : 'OSS'} Firefox`, - agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, + agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, @@ -118,7 +118,7 @@ for (const testSuite of testSuites) { IS_XPACK ? 'xpack' : 'oss' }_accessibility.sh`, label: `${IS_XPACK ? 'Default' : 'OSS'} Accessibility`, - agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, + agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 9e9990816ad1d..534300cce3988 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -19,7 +19,7 @@ steps: label: 'Default CI Group' parallelism: 13 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 250 key: default-cigroup @@ -31,7 +31,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -67,7 +67,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -89,7 +89,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -100,7 +100,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -111,7 +111,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 34db52772e619..b99473c23d746 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -15,9 +15,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -29,7 +29,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -65,7 +65,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -87,7 +87,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -98,7 +98,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -109,7 +109,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -156,7 +156,7 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' agents: - queue: c2-4 + queue: c2-8 key: checks timeout_in_minutes: 120 diff --git a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh index 1d73d1748ddf7..5827fd5eb2284 100755 --- a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh +++ b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh @@ -11,9 +11,27 @@ checks-reporter-with-killswitch "Build TS Refs" \ --no-cache \ --force -echo --- Check Types checks-reporter-with-killswitch "Check Types" \ - node scripts/type_check + node scripts/type_check &> target/check_types.log & +check_types_pid=$! + +node --max-old-space-size=12000 scripts/build_api_docs &> target/build_api_docs.log & +api_docs_pid=$! + +wait $check_types_pid +check_types_exit=$? + +wait $api_docs_pid +api_docs_exit=$? + +echo --- Check Types +cat target/check_types.log +if [[ "$check_types_exit" != "0" ]]; then echo "^^^ +++"; fi echo --- Building api docs -node --max-old-space-size=12000 scripts/build_api_docs +cat target/build_api_docs.log +if [[ "$api_docs_exit" != "0" ]]; then echo "^^^ +++"; fi + +if [[ "${api_docs_exit}${check_types_exit}" != "00" ]]; then + exit 1 +fi diff --git a/.ci/ci_groups.yml b/.ci/ci_groups.yml index 9c3a039f51166..1be6e8c196a2d 100644 --- a/.ci/ci_groups.yml +++ b/.ci/ci_groups.yml @@ -25,4 +25,18 @@ xpack: - ciGroup11 - ciGroup12 - ciGroup13 + - ciGroup14 + - ciGroup15 + - ciGroup16 + - ciGroup17 + - ciGroup18 + - ciGroup19 + - ciGroup20 + - ciGroup21 + - ciGroup22 + - ciGroup23 + - ciGroup24 + - ciGroup25 + - ciGroup26 + - ciGroup27 - ciGroupDocker diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 4787d7b9ee532..c906697021ecf 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -22,7 +22,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function () { - this.tags('ciGroup7'); + this.tags('ciGroup9'); loadTestFile(require.resolve('./_create_index_pattern_wizard')); loadTestFile(require.resolve('./_index_pattern_create_delete')); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 3bc4da0163909..68b95f3521a24 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -74,8 +74,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_metric_chart')); }); - describe('visualize ciGroup4', function () { - this.tags('ciGroup4'); + describe('visualize ciGroup1', function () { + this.tags('ciGroup1'); loadTestFile(require.resolve('./_pie_chart')); loadTestFile(require.resolve('./_shared_item')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts index a1f15c0db75fd..211fe9ec26863 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts @@ -58,7 +58,7 @@ export async function tearDown(getService: FtrProviderContext['getService']) { // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('alerting api integration security and spaces enabled', function () { - this.tags('ciGroup5'); + this.tags('ciGroup17'); loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index c3d08ba306692..56b2042dc4854 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./search')); loadTestFile(require.resolve('./es')); @@ -27,12 +27,12 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./maps')); loadTestFile(require.resolve('./security_solution')); loadTestFile(require.resolve('./lens')); - loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); loadTestFile(require.resolve('./searchprofiler')); loadTestFile(require.resolve('./painless_lab')); loadTestFile(require.resolve('./file_upload')); + loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index e190e02d9bdea..eb81d8245dbff 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. diff --git a/x-pack/test/api_integration/apis/spaces/index.ts b/x-pack/test/api_integration/apis/spaces/index.ts index 7267329249b22..3ca0040e39ec9 100644 --- a/x-pack/test/api_integration/apis/spaces/index.ts +++ b/x-pack/test/api_integration/apis/spaces/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('spaces', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./get_active_space')); loadTestFile(require.resolve('./saved_objects')); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts index 3fb7d7a29af39..ce2f59a115e69 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -12,7 +12,7 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: basic', function () { // Fastest ciGroup for the moment. - this.tags('ciGroup13'); + this.tags('ciGroup27'); before(async () => { await createSpacesAndUsers(getService); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index cd4b062c065a0..1605003bf7015 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -11,8 +11,7 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup13'); + this.tags('ciGroup25'); before(async () => { await createSpacesAndUsers(getService); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts index cebd20b698c26..85cc484146032 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection exceptions data types and operators', function () { describe('', function () { - this.tags('ciGroup11'); + this.tags('ciGroup23'); loadTestFile(require.resolve('./date')); loadTestFile(require.resolve('./double')); @@ -20,7 +20,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { }); describe('', function () { - this.tags('ciGroup12'); + this.tags('ciGroup24'); loadTestFile(require.resolve('./ip')); loadTestFile(require.resolve('./ip_array')); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 73c9b83de917f..59211ecf37f2d 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('dashboard', function () { - this.tags('ciGroup7'); + this.tags('ciGroup19'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index af117a2182034..9eda11bc6e6fb 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('discover', function () { - this.tags('ciGroup1'); + this.tags('ciGroup25'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index f9e4835f044af..79f9b8f645c1a 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -50,10 +50,6 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid describe('', function () { this.tags(['ciGroup4', 'skipFirefox']); - loadTestFile(require.resolve('./add_to_dashboard')); - loadTestFile(require.resolve('./table')); - loadTestFile(require.resolve('./runtime_fields')); - loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./colors')); loadTestFile(require.resolve('./chart_data')); loadTestFile(require.resolve('./time_shift')); @@ -69,5 +65,14 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid // has to be last one in the suite because it overrides saved objects loadTestFile(require.resolve('./rollup')); }); + + describe('', function () { + this.tags(['ciGroup16', 'skipFirefox']); + + loadTestFile(require.resolve('./add_to_dashboard')); + loadTestFile(require.resolve('./table')); + loadTestFile(require.resolve('./runtime_fields')); + loadTestFile(require.resolve('./dashboard')); + }); }); } diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 6a2a843682f26..b85859bf2d5d3 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -73,8 +73,13 @@ export default function ({ loadTestFile, getService }) { }); describe('', function () { - this.tags('ciGroup10'); + this.tags('ciGroup22'); loadTestFile(require.resolve('./es_geo_grid_source')); + loadTestFile(require.resolve('./embeddable')); + }); + + describe('', function () { + this.tags('ciGroup10'); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); loadTestFile(require.resolve('./mapbox_styles')); @@ -83,7 +88,6 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./add_layer_panel')); loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); - loadTestFile(require.resolve('./embeddable')); loadTestFile(require.resolve('./visualize_create_menu')); loadTestFile(require.resolve('./discover')); }); diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index 2e3a29d50dd11..493813daa4f72 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('machine learning', function () { describe('', function () { - this.tags('ciGroup3'); - before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -47,12 +45,19 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./pages')); - loadTestFile(require.resolve('./anomaly_detection')); - loadTestFile(require.resolve('./data_visualizer')); - loadTestFile(require.resolve('./data_frame_analytics')); - loadTestFile(require.resolve('./model_management')); + describe('', function () { + this.tags('ciGroup15'); + loadTestFile(require.resolve('./permissions')); + loadTestFile(require.resolve('./pages')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./model_management')); + }); + + describe('', function () { + this.tags('ciGroup26'); + loadTestFile(require.resolve('./anomaly_detection')); + }); }); describe('', function () { diff --git a/x-pack/test/functional/apps/security/index.ts b/x-pack/test/functional/apps/security/index.ts index 3b4c6989d38fa..fc9caafbabb29 100644 --- a/x-pack/test/functional/apps/security/index.ts +++ b/x-pack/test/functional/apps/security/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app', function () { - this.tags('ciGroup4'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./doc_level_security_roles')); diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 90bb95fd6b3e8..4a9aafb072852 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -16,7 +16,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const transform = getService('transform'); describe('transform', function () { - this.tags(['ciGroup9', 'transform']); + this.tags(['ciGroup21', 'transform']); before(async () => { await transform.securityCommon.createTransformRoles(); diff --git a/x-pack/test/functional_basic/apps/ml/index.ts b/x-pack/test/functional_basic/apps/ml/index.ts index ed1ab4f417584..af2fdc8c45f29 100644 --- a/x-pack/test/functional_basic/apps/ml/index.ts +++ b/x-pack/test/functional_basic/apps/ml/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning basic license', function () { - this.tags(['ciGroup2', 'skipFirefox', 'mlqa']); + this.tags(['ciGroup14', 'skipFirefox', 'mlqa']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index 5412f9d9bdfed..740b9d91927bf 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -13,7 +13,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const supertest = getService('supertest'); describe('saved objects security and spaces enabled', function () { - this.tags('ciGroup8'); + this.tags('ciGroup20'); before(async () => { await createUsersAndRoles(es, supertest); diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index 7a82574f34b6e..fbf0954382dd1 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -11,7 +11,7 @@ import { createUsersAndRoles } from '../../common/lib'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('saved objects tagging - functional tests', function () { - this.tags('ciGroup2'); + this.tags('ciGroup14'); before(async () => { await createUsersAndRoles(getService); diff --git a/x-pack/test/security_api_integration/tests/kerberos/index.ts b/x-pack/test/security_api_integration/tests/kerberos/index.ts index 3faec0badd89e..39aac8cc4ca2f 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { - this.tags('ciGroup6'); + this.tags('ciGroup16'); loadTestFile(require.resolve('./kerberos_login')); }); diff --git a/x-pack/test/security_api_integration/tests/saml/index.ts b/x-pack/test/security_api_integration/tests/saml/index.ts index 375864c71432d..dbabb835ee980 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./saml_login')); }); diff --git a/x-pack/test/security_api_integration/tests/session_idle/index.ts b/x-pack/test/security_api_integration/tests/session_idle/index.ts index bbf811de70db4..76457ee7ad0c7 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/index.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Idle', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./cleanup')); loadTestFile(require.resolve('./extension')); From 30de97bc2d8b8f9ec1f41dafdcef100e39c78ecf Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 10 Nov 2021 10:34:38 -0500 Subject: [PATCH 92/98] Revert "[CI] Rebalance/split cigroups and speed up overall CI time (#113676)" This reverts commit 9888b7fec3f84e76d8f32eb1757a71fa731c867d. --- .buildkite/pipelines/es_snapshots/verify.yml | 6 ++--- .buildkite/pipelines/flaky_tests/runner.js | 6 ++--- .buildkite/pipelines/hourly.yml | 12 +++++----- .buildkite/pipelines/pull_request/base.yml | 16 ++++++------- .../type_check_plugin_public_api_docs.sh | 24 +++---------------- .ci/ci_groups.yml | 14 ----------- test/functional/apps/management/index.ts | 2 +- test/functional/apps/visualize/index.ts | 4 ++-- .../security_and_spaces/tests/index.ts | 2 +- x-pack/test/api_integration/apis/index.ts | 4 ++-- .../api_integration/apis/security/index.ts | 2 +- .../test/api_integration/apis/spaces/index.ts | 2 +- .../security_and_spaces/tests/basic/index.ts | 2 +- .../security_and_spaces/tests/trial/index.ts | 3 ++- .../exception_operators_data_types/index.ts | 4 ++-- .../test/functional/apps/dashboard/index.ts | 2 +- x-pack/test/functional/apps/discover/index.ts | 2 +- x-pack/test/functional/apps/lens/index.ts | 13 ++++------ x-pack/test/functional/apps/maps/index.js | 8 ++----- x-pack/test/functional/apps/ml/index.ts | 21 +++++++--------- x-pack/test/functional/apps/security/index.ts | 2 +- .../test/functional/apps/transform/index.ts | 2 +- x-pack/test/functional_basic/apps/ml/index.ts | 2 +- .../security_and_spaces/apis/index.ts | 2 +- .../functional/tests/index.ts | 2 +- .../tests/kerberos/index.ts | 2 +- .../tests/saml/index.ts | 2 +- .../tests/session_idle/index.ts | 2 +- 28 files changed, 60 insertions(+), 105 deletions(-) diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 93f8765ec16f7..9cddade0b7482 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -29,7 +29,7 @@ steps: label: 'Default CI Group' parallelism: 13 agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -41,7 +41,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -77,7 +77,7 @@ steps: - command: .buildkite/scripts/steps/test/api_integration.sh label: 'API Integration Tests' agents: - queue: n2-2 + queue: jest timeout_in_minutes: 120 key: api-integration diff --git a/.buildkite/pipelines/flaky_tests/runner.js b/.buildkite/pipelines/flaky_tests/runner.js index 0c2db5c724f7b..b5ccab137fd01 100644 --- a/.buildkite/pipelines/flaky_tests/runner.js +++ b/.buildkite/pipelines/flaky_tests/runner.js @@ -78,7 +78,7 @@ for (const testSuite of testSuites) { steps.push({ command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, label: `Default CI Group ${CI_GROUP}`, - agents: { queue: 'n2-4' }, + agents: { queue: 'ci-group-6' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, @@ -103,7 +103,7 @@ for (const testSuite of testSuites) { steps.push({ command: `.buildkite/scripts/steps/functional/${IS_XPACK ? 'xpack' : 'oss'}_firefox.sh`, label: `${IS_XPACK ? 'Default' : 'OSS'} Firefox`, - agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, + agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, @@ -118,7 +118,7 @@ for (const testSuite of testSuites) { IS_XPACK ? 'xpack' : 'oss' }_accessibility.sh`, label: `${IS_XPACK ? 'Default' : 'OSS'} Accessibility`, - agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, + agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 534300cce3988..9e9990816ad1d 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -19,7 +19,7 @@ steps: label: 'Default CI Group' parallelism: 13 agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 250 key: default-cigroup @@ -31,7 +31,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -67,7 +67,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: @@ -89,7 +89,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: @@ -100,7 +100,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: @@ -111,7 +111,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index b99473c23d746..34db52772e619 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -15,9 +15,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 27 + parallelism: 13 agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -29,7 +29,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -65,7 +65,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: @@ -87,7 +87,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: @@ -98,7 +98,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: @@ -109,7 +109,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: n2-4 + queue: ci-group-6 depends_on: build timeout_in_minutes: 120 retry: @@ -156,7 +156,7 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' agents: - queue: c2-8 + queue: c2-4 key: checks timeout_in_minutes: 120 diff --git a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh index 5827fd5eb2284..1d73d1748ddf7 100755 --- a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh +++ b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh @@ -11,27 +11,9 @@ checks-reporter-with-killswitch "Build TS Refs" \ --no-cache \ --force -checks-reporter-with-killswitch "Check Types" \ - node scripts/type_check &> target/check_types.log & -check_types_pid=$! - -node --max-old-space-size=12000 scripts/build_api_docs &> target/build_api_docs.log & -api_docs_pid=$! - -wait $check_types_pid -check_types_exit=$? - -wait $api_docs_pid -api_docs_exit=$? - echo --- Check Types -cat target/check_types.log -if [[ "$check_types_exit" != "0" ]]; then echo "^^^ +++"; fi +checks-reporter-with-killswitch "Check Types" \ + node scripts/type_check echo --- Building api docs -cat target/build_api_docs.log -if [[ "$api_docs_exit" != "0" ]]; then echo "^^^ +++"; fi - -if [[ "${api_docs_exit}${check_types_exit}" != "00" ]]; then - exit 1 -fi +node --max-old-space-size=12000 scripts/build_api_docs diff --git a/.ci/ci_groups.yml b/.ci/ci_groups.yml index 1be6e8c196a2d..9c3a039f51166 100644 --- a/.ci/ci_groups.yml +++ b/.ci/ci_groups.yml @@ -25,18 +25,4 @@ xpack: - ciGroup11 - ciGroup12 - ciGroup13 - - ciGroup14 - - ciGroup15 - - ciGroup16 - - ciGroup17 - - ciGroup18 - - ciGroup19 - - ciGroup20 - - ciGroup21 - - ciGroup22 - - ciGroup23 - - ciGroup24 - - ciGroup25 - - ciGroup26 - - ciGroup27 - ciGroupDocker diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index c906697021ecf..4787d7b9ee532 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -22,7 +22,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function () { - this.tags('ciGroup9'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./_create_index_pattern_wizard')); loadTestFile(require.resolve('./_index_pattern_create_delete')); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 68b95f3521a24..3bc4da0163909 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -74,8 +74,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_metric_chart')); }); - describe('visualize ciGroup1', function () { - this.tags('ciGroup1'); + describe('visualize ciGroup4', function () { + this.tags('ciGroup4'); loadTestFile(require.resolve('./_pie_chart')); loadTestFile(require.resolve('./_shared_item')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts index 211fe9ec26863..a1f15c0db75fd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts @@ -58,7 +58,7 @@ export async function tearDown(getService: FtrProviderContext['getService']) { // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('alerting api integration security and spaces enabled', function () { - this.tags('ciGroup17'); + this.tags('ciGroup5'); loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index 56b2042dc4854..c3d08ba306692 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { - this.tags('ciGroup18'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./search')); loadTestFile(require.resolve('./es')); @@ -27,12 +27,12 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./maps')); loadTestFile(require.resolve('./security_solution')); loadTestFile(require.resolve('./lens')); + loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); loadTestFile(require.resolve('./searchprofiler')); loadTestFile(require.resolve('./painless_lab')); loadTestFile(require.resolve('./file_upload')); - loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index eb81d8245dbff..e190e02d9bdea 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security', function () { - this.tags('ciGroup18'); + this.tags('ciGroup6'); // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. diff --git a/x-pack/test/api_integration/apis/spaces/index.ts b/x-pack/test/api_integration/apis/spaces/index.ts index 3ca0040e39ec9..7267329249b22 100644 --- a/x-pack/test/api_integration/apis/spaces/index.ts +++ b/x-pack/test/api_integration/apis/spaces/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('spaces', function () { - this.tags('ciGroup18'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./get_active_space')); loadTestFile(require.resolve('./saved_objects')); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts index ce2f59a115e69..3fb7d7a29af39 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -12,7 +12,7 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: basic', function () { // Fastest ciGroup for the moment. - this.tags('ciGroup27'); + this.tags('ciGroup13'); before(async () => { await createSpacesAndUsers(getService); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index 1605003bf7015..cd4b062c065a0 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -11,7 +11,8 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: trial', function () { - this.tags('ciGroup25'); + // Fastest ciGroup for the moment. + this.tags('ciGroup13'); before(async () => { await createSpacesAndUsers(getService); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts index 85cc484146032..cebd20b698c26 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection exceptions data types and operators', function () { describe('', function () { - this.tags('ciGroup23'); + this.tags('ciGroup11'); loadTestFile(require.resolve('./date')); loadTestFile(require.resolve('./double')); @@ -20,7 +20,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { }); describe('', function () { - this.tags('ciGroup24'); + this.tags('ciGroup12'); loadTestFile(require.resolve('./ip')); loadTestFile(require.resolve('./ip_array')); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 59211ecf37f2d..73c9b83de917f 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('dashboard', function () { - this.tags('ciGroup19'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 9eda11bc6e6fb..af117a2182034 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('discover', function () { - this.tags('ciGroup25'); + this.tags('ciGroup1'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 79f9b8f645c1a..f9e4835f044af 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -50,6 +50,10 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid describe('', function () { this.tags(['ciGroup4', 'skipFirefox']); + loadTestFile(require.resolve('./add_to_dashboard')); + loadTestFile(require.resolve('./table')); + loadTestFile(require.resolve('./runtime_fields')); + loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./colors')); loadTestFile(require.resolve('./chart_data')); loadTestFile(require.resolve('./time_shift')); @@ -65,14 +69,5 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid // has to be last one in the suite because it overrides saved objects loadTestFile(require.resolve('./rollup')); }); - - describe('', function () { - this.tags(['ciGroup16', 'skipFirefox']); - - loadTestFile(require.resolve('./add_to_dashboard')); - loadTestFile(require.resolve('./table')); - loadTestFile(require.resolve('./runtime_fields')); - loadTestFile(require.resolve('./dashboard')); - }); }); } diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index b85859bf2d5d3..6a2a843682f26 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -72,14 +72,9 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./full_screen_mode')); }); - describe('', function () { - this.tags('ciGroup22'); - loadTestFile(require.resolve('./es_geo_grid_source')); - loadTestFile(require.resolve('./embeddable')); - }); - describe('', function () { this.tags('ciGroup10'); + loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); loadTestFile(require.resolve('./mapbox_styles')); @@ -88,6 +83,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./add_layer_panel')); loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); + loadTestFile(require.resolve('./embeddable')); loadTestFile(require.resolve('./visualize_create_menu')); loadTestFile(require.resolve('./discover')); }); diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index 493813daa4f72..2e3a29d50dd11 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -13,6 +13,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('machine learning', function () { describe('', function () { + this.tags('ciGroup3'); + before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -45,19 +47,12 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - describe('', function () { - this.tags('ciGroup15'); - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./pages')); - loadTestFile(require.resolve('./data_visualizer')); - loadTestFile(require.resolve('./data_frame_analytics')); - loadTestFile(require.resolve('./model_management')); - }); - - describe('', function () { - this.tags('ciGroup26'); - loadTestFile(require.resolve('./anomaly_detection')); - }); + loadTestFile(require.resolve('./permissions')); + loadTestFile(require.resolve('./pages')); + loadTestFile(require.resolve('./anomaly_detection')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./model_management')); }); describe('', function () { diff --git a/x-pack/test/functional/apps/security/index.ts b/x-pack/test/functional/apps/security/index.ts index fc9caafbabb29..3b4c6989d38fa 100644 --- a/x-pack/test/functional/apps/security/index.ts +++ b/x-pack/test/functional/apps/security/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app', function () { - this.tags('ciGroup7'); + this.tags('ciGroup4'); loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./doc_level_security_roles')); diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 4a9aafb072852..90bb95fd6b3e8 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -16,7 +16,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const transform = getService('transform'); describe('transform', function () { - this.tags(['ciGroup21', 'transform']); + this.tags(['ciGroup9', 'transform']); before(async () => { await transform.securityCommon.createTransformRoles(); diff --git a/x-pack/test/functional_basic/apps/ml/index.ts b/x-pack/test/functional_basic/apps/ml/index.ts index af2fdc8c45f29..ed1ab4f417584 100644 --- a/x-pack/test/functional_basic/apps/ml/index.ts +++ b/x-pack/test/functional_basic/apps/ml/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning basic license', function () { - this.tags(['ciGroup14', 'skipFirefox', 'mlqa']); + this.tags(['ciGroup2', 'skipFirefox', 'mlqa']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index 740b9d91927bf..5412f9d9bdfed 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -13,7 +13,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const supertest = getService('supertest'); describe('saved objects security and spaces enabled', function () { - this.tags('ciGroup20'); + this.tags('ciGroup8'); before(async () => { await createUsersAndRoles(es, supertest); diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index fbf0954382dd1..7a82574f34b6e 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -11,7 +11,7 @@ import { createUsersAndRoles } from '../../common/lib'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('saved objects tagging - functional tests', function () { - this.tags('ciGroup14'); + this.tags('ciGroup2'); before(async () => { await createUsersAndRoles(getService); diff --git a/x-pack/test/security_api_integration/tests/kerberos/index.ts b/x-pack/test/security_api_integration/tests/kerberos/index.ts index 39aac8cc4ca2f..3faec0badd89e 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { - this.tags('ciGroup16'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./kerberos_login')); }); diff --git a/x-pack/test/security_api_integration/tests/saml/index.ts b/x-pack/test/security_api_integration/tests/saml/index.ts index dbabb835ee980..375864c71432d 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { - this.tags('ciGroup18'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./saml_login')); }); diff --git a/x-pack/test/security_api_integration/tests/session_idle/index.ts b/x-pack/test/security_api_integration/tests/session_idle/index.ts index 76457ee7ad0c7..bbf811de70db4 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/index.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Idle', function () { - this.tags('ciGroup18'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./cleanup')); loadTestFile(require.resolve('./extension')); From 60a922152741a9d197fcb083381481c705ca2e96 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Wed, 10 Nov 2021 17:16:44 +0100 Subject: [PATCH 93/98] Add missing await (#118171) --- .../rule_execution_log/event_log_adapter/event_log_adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts index e5660da8d4cf4..8b55339aa9f02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts @@ -104,7 +104,7 @@ export class EventLogAdapter implements IRuleExecutionLogClient { await this.savedObjectsAdapter.logStatusChange(args); if (args.metrics) { - this.logExecutionMetrics({ + await this.logExecutionMetrics({ ruleId: args.ruleId, ruleName: args.ruleName, ruleType: args.ruleType, From 5cccf0cdd6ce3a7601ab3a0c0b04edab6db1bae1 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Wed, 10 Nov 2021 17:17:07 +0100 Subject: [PATCH 94/98] [Reporting] Optimize visualizations awaiter performance (#118012) --- .../chromium/driver/chromium_driver.ts | 40 +++++-------------- .../screenshots/wait_for_visualizations.ts | 29 ++++++++++---- 2 files changed, 31 insertions(+), 38 deletions(-) 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 e7c2b68ba2712..0947d24f827c2 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 @@ -210,36 +210,16 @@ export class HeadlessChromiumDriver { return resp; } - public async waitFor( - { - fn, - args, - toEqual, - timeout, - }: { - fn: EvaluateFn; - args: SerializableOrJSHandle[]; - toEqual: number; - timeout: number; - }, - context: EvaluateMetaOpts, - logger: LevelLogger - ): Promise { - const startTime = Date.now(); - - while (true) { - const result = await this.evaluate({ fn, args }, context, logger); - if (result === toEqual) { - return; - } - - if (Date.now() - startTime > timeout) { - throw new Error( - `Timed out waiting for the items selected to equal ${toEqual}. Found: ${result}. Context: ${context.context}` - ); - } - await new Promise((r) => setTimeout(r, WAIT_FOR_DELAY_MS)); - } + public async waitFor({ + fn, + args, + timeout, + }: { + fn: EvaluateFn; + args: SerializableOrJSHandle[]; + timeout: number; + }): Promise { + await this.page.waitForFunction(fn, { timeout, polling: WAIT_FOR_DELAY_MS }, ...args); } public async setViewport( diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts index d4bf1db2a0c5a..10a53b238d892 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts @@ -11,10 +11,23 @@ import { HeadlessChromiumDriver } from '../../browsers'; import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; -type SelectorArgs = Record; +interface CompletedItemsCountParameters { + context: string; + count: number; + renderCompleteSelector: string; +} -const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => { - return document.querySelectorAll(renderCompleteSelector).length; +const getCompletedItemsCount = ({ + context, + count, + renderCompleteSelector, +}: CompletedItemsCountParameters) => { + const { length } = document.querySelectorAll(renderCompleteSelector); + + // eslint-disable-next-line no-console + console.debug(`evaluate ${context}: waitng for ${count} elements, got ${length}.`); + + return length >= count; }; /* @@ -40,11 +53,11 @@ export const waitForVisualizations = async ( ); try { - await browser.waitFor( - { fn: getCompletedItemsCount, args: [{ renderCompleteSelector }], toEqual, timeout }, - { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, - logger - ); + await browser.waitFor({ + fn: getCompletedItemsCount, + args: [{ renderCompleteSelector, context: CONTEXT_WAITFORELEMENTSTOBEINDOM, count: toEqual }], + timeout, + }); logger.debug(`found ${toEqual} rendered elements in the DOM`); } catch (err) { From 68f46a5c8ebe0fb44e9ef140eeebb25c25a73f44 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Wed, 10 Nov 2021 08:48:57 -0800 Subject: [PATCH 95/98] Update web crawler link to lead directly to the crawler page (#118111) --- .../sections/epm/screens/home/available_packages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index e54ab0d9ecd46..62f911ffdbbb7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -348,7 +348,7 @@ export const AvailablePackages: React.FC = memo(() => { } - href={addBasePath('/app/enterprise_search/app_search')} + href={addBasePath('/app/enterprise_search/app_search/engines/new?method=crawler')} title={i18n.translate('xpack.fleet.featuredSearchTitle', { defaultMessage: 'Web site crawler', })} From 8ba9ebf59294266f41264248a94f3bb420d9de4c Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 10 Nov 2021 12:01:03 -0500 Subject: [PATCH 96/98] [CI] Rebalance/split cigroups and speed up overall CI time (#118191) --- .buildkite/pipelines/es_snapshots/verify.yml | 8 +++---- .buildkite/pipelines/flaky_tests/pipeline.js | 2 +- .buildkite/pipelines/flaky_tests/runner.js | 6 ++--- .buildkite/pipelines/hourly.yml | 14 +++++------ .buildkite/pipelines/pull_request/base.yml | 16 ++++++------- .../type_check_plugin_public_api_docs.sh | 24 ++++++++++++++++--- .ci/ci_groups.yml | 14 +++++++++++ test/functional/apps/management/index.ts | 2 +- test/functional/apps/visualize/index.ts | 4 ++-- .../security_and_spaces/tests/index.ts | 2 +- x-pack/test/api_integration/apis/index.ts | 4 ++-- .../api_integration/apis/security/index.ts | 2 +- .../test/api_integration/apis/spaces/index.ts | 2 +- .../security_and_spaces/tests/basic/index.ts | 2 +- .../security_and_spaces/tests/trial/index.ts | 3 +-- .../exception_operators_data_types/index.ts | 4 ++-- .../test/functional/apps/dashboard/index.ts | 2 +- x-pack/test/functional/apps/discover/index.ts | 2 +- x-pack/test/functional/apps/lens/index.ts | 13 ++++++---- x-pack/test/functional/apps/maps/index.js | 8 +++++-- x-pack/test/functional/apps/ml/index.ts | 21 +++++++++------- x-pack/test/functional/apps/security/index.ts | 2 +- .../test/functional/apps/transform/index.ts | 2 +- x-pack/test/functional_basic/apps/ml/index.ts | 2 +- .../security_and_spaces/apis/index.ts | 2 +- .../functional/tests/index.ts | 2 +- .../tests/kerberos/index.ts | 2 +- .../tests/saml/index.ts | 2 +- .../tests/session_idle/index.ts | 2 +- 29 files changed, 108 insertions(+), 63 deletions(-) diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 9cddade0b7482..7d700b1e0f489 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -27,9 +27,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -41,7 +41,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -77,7 +77,7 @@ steps: - command: .buildkite/scripts/steps/test/api_integration.sh label: 'API Integration Tests' agents: - queue: jest + queue: n2-2 timeout_in_minutes: 120 key: api-integration diff --git a/.buildkite/pipelines/flaky_tests/pipeline.js b/.buildkite/pipelines/flaky_tests/pipeline.js index 37e8a97406eb5..bf4abb9ff4c89 100644 --- a/.buildkite/pipelines/flaky_tests/pipeline.js +++ b/.buildkite/pipelines/flaky_tests/pipeline.js @@ -8,7 +8,7 @@ const stepInput = (key, nameOfSuite) => { }; const OSS_CI_GROUPS = 12; -const XPACK_CI_GROUPS = 13; +const XPACK_CI_GROUPS = 27; const inputs = [ { diff --git a/.buildkite/pipelines/flaky_tests/runner.js b/.buildkite/pipelines/flaky_tests/runner.js index b5ccab137fd01..0c2db5c724f7b 100644 --- a/.buildkite/pipelines/flaky_tests/runner.js +++ b/.buildkite/pipelines/flaky_tests/runner.js @@ -78,7 +78,7 @@ for (const testSuite of testSuites) { steps.push({ command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, label: `Default CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-6' }, + agents: { queue: 'n2-4' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, @@ -103,7 +103,7 @@ for (const testSuite of testSuites) { steps.push({ command: `.buildkite/scripts/steps/functional/${IS_XPACK ? 'xpack' : 'oss'}_firefox.sh`, label: `${IS_XPACK ? 'Default' : 'OSS'} Firefox`, - agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, + agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, @@ -118,7 +118,7 @@ for (const testSuite of testSuites) { IS_XPACK ? 'xpack' : 'oss' }_accessibility.sh`, label: `${IS_XPACK ? 'Default' : 'OSS'} Accessibility`, - agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, + agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 9e9990816ad1d..bc9644820784d 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -17,9 +17,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 250 key: default-cigroup @@ -31,7 +31,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -67,7 +67,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -89,7 +89,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -100,7 +100,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -111,7 +111,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 34db52772e619..b99473c23d746 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -15,9 +15,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -29,7 +29,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -65,7 +65,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -87,7 +87,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -98,7 +98,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -109,7 +109,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -156,7 +156,7 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' agents: - queue: c2-4 + queue: c2-8 key: checks timeout_in_minutes: 120 diff --git a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh index 1d73d1748ddf7..5827fd5eb2284 100755 --- a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh +++ b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh @@ -11,9 +11,27 @@ checks-reporter-with-killswitch "Build TS Refs" \ --no-cache \ --force -echo --- Check Types checks-reporter-with-killswitch "Check Types" \ - node scripts/type_check + node scripts/type_check &> target/check_types.log & +check_types_pid=$! + +node --max-old-space-size=12000 scripts/build_api_docs &> target/build_api_docs.log & +api_docs_pid=$! + +wait $check_types_pid +check_types_exit=$? + +wait $api_docs_pid +api_docs_exit=$? + +echo --- Check Types +cat target/check_types.log +if [[ "$check_types_exit" != "0" ]]; then echo "^^^ +++"; fi echo --- Building api docs -node --max-old-space-size=12000 scripts/build_api_docs +cat target/build_api_docs.log +if [[ "$api_docs_exit" != "0" ]]; then echo "^^^ +++"; fi + +if [[ "${api_docs_exit}${check_types_exit}" != "00" ]]; then + exit 1 +fi diff --git a/.ci/ci_groups.yml b/.ci/ci_groups.yml index 9c3a039f51166..1be6e8c196a2d 100644 --- a/.ci/ci_groups.yml +++ b/.ci/ci_groups.yml @@ -25,4 +25,18 @@ xpack: - ciGroup11 - ciGroup12 - ciGroup13 + - ciGroup14 + - ciGroup15 + - ciGroup16 + - ciGroup17 + - ciGroup18 + - ciGroup19 + - ciGroup20 + - ciGroup21 + - ciGroup22 + - ciGroup23 + - ciGroup24 + - ciGroup25 + - ciGroup26 + - ciGroup27 - ciGroupDocker diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 4787d7b9ee532..c906697021ecf 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -22,7 +22,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function () { - this.tags('ciGroup7'); + this.tags('ciGroup9'); loadTestFile(require.resolve('./_create_index_pattern_wizard')); loadTestFile(require.resolve('./_index_pattern_create_delete')); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 3bc4da0163909..68b95f3521a24 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -74,8 +74,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_metric_chart')); }); - describe('visualize ciGroup4', function () { - this.tags('ciGroup4'); + describe('visualize ciGroup1', function () { + this.tags('ciGroup1'); loadTestFile(require.resolve('./_pie_chart')); loadTestFile(require.resolve('./_shared_item')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts index a1f15c0db75fd..211fe9ec26863 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts @@ -58,7 +58,7 @@ export async function tearDown(getService: FtrProviderContext['getService']) { // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('alerting api integration security and spaces enabled', function () { - this.tags('ciGroup5'); + this.tags('ciGroup17'); loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index c3d08ba306692..56b2042dc4854 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./search')); loadTestFile(require.resolve('./es')); @@ -27,12 +27,12 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./maps')); loadTestFile(require.resolve('./security_solution')); loadTestFile(require.resolve('./lens')); - loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); loadTestFile(require.resolve('./searchprofiler')); loadTestFile(require.resolve('./painless_lab')); loadTestFile(require.resolve('./file_upload')); + loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index e190e02d9bdea..eb81d8245dbff 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. diff --git a/x-pack/test/api_integration/apis/spaces/index.ts b/x-pack/test/api_integration/apis/spaces/index.ts index 7267329249b22..3ca0040e39ec9 100644 --- a/x-pack/test/api_integration/apis/spaces/index.ts +++ b/x-pack/test/api_integration/apis/spaces/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('spaces', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./get_active_space')); loadTestFile(require.resolve('./saved_objects')); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts index 3fb7d7a29af39..ce2f59a115e69 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -12,7 +12,7 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: basic', function () { // Fastest ciGroup for the moment. - this.tags('ciGroup13'); + this.tags('ciGroup27'); before(async () => { await createSpacesAndUsers(getService); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index cd4b062c065a0..1605003bf7015 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -11,8 +11,7 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup13'); + this.tags('ciGroup25'); before(async () => { await createSpacesAndUsers(getService); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts index cebd20b698c26..85cc484146032 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection exceptions data types and operators', function () { describe('', function () { - this.tags('ciGroup11'); + this.tags('ciGroup23'); loadTestFile(require.resolve('./date')); loadTestFile(require.resolve('./double')); @@ -20,7 +20,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { }); describe('', function () { - this.tags('ciGroup12'); + this.tags('ciGroup24'); loadTestFile(require.resolve('./ip')); loadTestFile(require.resolve('./ip_array')); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 73c9b83de917f..59211ecf37f2d 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('dashboard', function () { - this.tags('ciGroup7'); + this.tags('ciGroup19'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index af117a2182034..9eda11bc6e6fb 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('discover', function () { - this.tags('ciGroup1'); + this.tags('ciGroup25'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index f9e4835f044af..79f9b8f645c1a 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -50,10 +50,6 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid describe('', function () { this.tags(['ciGroup4', 'skipFirefox']); - loadTestFile(require.resolve('./add_to_dashboard')); - loadTestFile(require.resolve('./table')); - loadTestFile(require.resolve('./runtime_fields')); - loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./colors')); loadTestFile(require.resolve('./chart_data')); loadTestFile(require.resolve('./time_shift')); @@ -69,5 +65,14 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid // has to be last one in the suite because it overrides saved objects loadTestFile(require.resolve('./rollup')); }); + + describe('', function () { + this.tags(['ciGroup16', 'skipFirefox']); + + loadTestFile(require.resolve('./add_to_dashboard')); + loadTestFile(require.resolve('./table')); + loadTestFile(require.resolve('./runtime_fields')); + loadTestFile(require.resolve('./dashboard')); + }); }); } diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 6a2a843682f26..b85859bf2d5d3 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -73,8 +73,13 @@ export default function ({ loadTestFile, getService }) { }); describe('', function () { - this.tags('ciGroup10'); + this.tags('ciGroup22'); loadTestFile(require.resolve('./es_geo_grid_source')); + loadTestFile(require.resolve('./embeddable')); + }); + + describe('', function () { + this.tags('ciGroup10'); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); loadTestFile(require.resolve('./mapbox_styles')); @@ -83,7 +88,6 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./add_layer_panel')); loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); - loadTestFile(require.resolve('./embeddable')); loadTestFile(require.resolve('./visualize_create_menu')); loadTestFile(require.resolve('./discover')); }); diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index 2e3a29d50dd11..493813daa4f72 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('machine learning', function () { describe('', function () { - this.tags('ciGroup3'); - before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -47,12 +45,19 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./pages')); - loadTestFile(require.resolve('./anomaly_detection')); - loadTestFile(require.resolve('./data_visualizer')); - loadTestFile(require.resolve('./data_frame_analytics')); - loadTestFile(require.resolve('./model_management')); + describe('', function () { + this.tags('ciGroup15'); + loadTestFile(require.resolve('./permissions')); + loadTestFile(require.resolve('./pages')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./model_management')); + }); + + describe('', function () { + this.tags('ciGroup26'); + loadTestFile(require.resolve('./anomaly_detection')); + }); }); describe('', function () { diff --git a/x-pack/test/functional/apps/security/index.ts b/x-pack/test/functional/apps/security/index.ts index 3b4c6989d38fa..fc9caafbabb29 100644 --- a/x-pack/test/functional/apps/security/index.ts +++ b/x-pack/test/functional/apps/security/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app', function () { - this.tags('ciGroup4'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./doc_level_security_roles')); diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 90bb95fd6b3e8..4a9aafb072852 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -16,7 +16,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const transform = getService('transform'); describe('transform', function () { - this.tags(['ciGroup9', 'transform']); + this.tags(['ciGroup21', 'transform']); before(async () => { await transform.securityCommon.createTransformRoles(); diff --git a/x-pack/test/functional_basic/apps/ml/index.ts b/x-pack/test/functional_basic/apps/ml/index.ts index ed1ab4f417584..af2fdc8c45f29 100644 --- a/x-pack/test/functional_basic/apps/ml/index.ts +++ b/x-pack/test/functional_basic/apps/ml/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning basic license', function () { - this.tags(['ciGroup2', 'skipFirefox', 'mlqa']); + this.tags(['ciGroup14', 'skipFirefox', 'mlqa']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index 5412f9d9bdfed..740b9d91927bf 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -13,7 +13,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const supertest = getService('supertest'); describe('saved objects security and spaces enabled', function () { - this.tags('ciGroup8'); + this.tags('ciGroup20'); before(async () => { await createUsersAndRoles(es, supertest); diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index 7a82574f34b6e..fbf0954382dd1 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -11,7 +11,7 @@ import { createUsersAndRoles } from '../../common/lib'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('saved objects tagging - functional tests', function () { - this.tags('ciGroup2'); + this.tags('ciGroup14'); before(async () => { await createUsersAndRoles(getService); diff --git a/x-pack/test/security_api_integration/tests/kerberos/index.ts b/x-pack/test/security_api_integration/tests/kerberos/index.ts index 3faec0badd89e..39aac8cc4ca2f 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { - this.tags('ciGroup6'); + this.tags('ciGroup16'); loadTestFile(require.resolve('./kerberos_login')); }); diff --git a/x-pack/test/security_api_integration/tests/saml/index.ts b/x-pack/test/security_api_integration/tests/saml/index.ts index 375864c71432d..dbabb835ee980 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./saml_login')); }); diff --git a/x-pack/test/security_api_integration/tests/session_idle/index.ts b/x-pack/test/security_api_integration/tests/session_idle/index.ts index bbf811de70db4..76457ee7ad0c7 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/index.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Idle', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./cleanup')); loadTestFile(require.resolve('./extension')); From 6b8145479502a22bbb9107666a84a146e7a35a1f Mon Sep 17 00:00:00 2001 From: ymao1 Date: Wed, 10 Nov 2021 12:10:43 -0500 Subject: [PATCH 97/98] Removing focusable element in tags badge (#118062) --- .../application/sections/alerts_list/components/alerts_list.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index b48d5a6a3629f..162f41605e91e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -438,6 +438,7 @@ export const AlertsList: React.FunctionComponent = () => { color="hollow" iconType="tag" iconSide="left" + tabIndex={-1} onClick={() => setTagPopoverOpenIndex(item.index)} onClickAriaLabel="Tags" iconOnClick={() => setTagPopoverOpenIndex(item.index)} From 42168954b422149dece5f4b5e3244cfa552ec326 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 10 Nov 2021 10:20:23 -0800 Subject: [PATCH 98/98] [DOCS] Renames index pattern in management and monitoring (#117939) * [DOCS] Renames index pattern in management, monitoring, and graph * [DOCS] Renames index pattern on landing page * Updates URL in doc link service * Update docs/management/advanced-options.asciidoc Co-authored-by: Lisa Cawley * Update docs/user/monitoring/kibana-alerts.asciidoc Co-authored-by: Lisa Cawley Co-authored-by: lcawl Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/index-extra-title-page.html | 2 +- docs/management/advanced-options.asciidoc | 18 ++--- .../management-rollup-index-pattern.png | Bin 59955 -> 0 bytes ...ns.asciidoc => manage-data-views.asciidoc} | 70 ++++++++++-------- .../managing-saved-objects.asciidoc | 8 +- docs/management/numeral.asciidoc | 2 +- .../create_and_manage_rollups.asciidoc | 28 +++---- docs/redirects.asciidoc | 5 ++ docs/user/graph/configuring-graph.asciidoc | 2 +- docs/user/management.asciidoc | 10 +-- docs/user/monitoring/kibana-alerts.asciidoc | 28 +++---- .../public/doc_links/doc_links_service.ts | 2 +- 12 files changed, 90 insertions(+), 85 deletions(-) delete mode 100644 docs/management/images/management-rollup-index-pattern.png rename docs/management/{manage-index-patterns.asciidoc => manage-data-views.asciidoc} (76%) diff --git a/docs/index-extra-title-page.html b/docs/index-extra-title-page.html index 2621848ebea8a..ff1c879c0f409 100644 --- a/docs/index-extra-title-page.html +++ b/docs/index-extra-title-page.html @@ -64,7 +64,7 @@
  • Create an index patternCreate a data view
  • diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 56b7eb09252ed..7e7ff1137794c 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -2,7 +2,7 @@ == Advanced Settings *Advanced Settings* control the behavior of {kib}. For example, you can change the format used to display dates, -specify the default index pattern, and set the precision for displayed decimal values. +specify the default data view, and set the precision for displayed decimal values. . Open the main menu, then click *Stack Management > Advanced Settings*. . Scroll or search for the setting. @@ -134,10 +134,6 @@ value by the maximum number of aggregations in each visualization. [[history-limit]]`history:limit`:: In fields that have history, such as query inputs, show this many recent values. -[[indexpattern-placeholder]]`indexPattern:placeholder`:: -The default placeholder value to use in -*Management > Index Patterns > Create Index Pattern*. - [[metafields]]`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. @@ -283,7 +279,7 @@ value is 5. [[context-tiebreakerfields]]`context:tieBreakerFields`:: A comma-separated list of fields to use for breaking a tie between documents that have the same timestamp value. The first field that is present and sortable -in the current index pattern is used. +in the current data view is used. [[defaultcolumns]]`defaultColumns`:: The columns that appear by default on the *Discover* page. The default is @@ -296,7 +292,7 @@ The number of rows to show in the *Discover* table. Specifies the maximum number of fields to show in the document column of the *Discover* table. [[discover-modify-columns-on-switch]]`discover:modifyColumnsOnSwitch`:: -When enabled, removes the columns that are not in the new index pattern. +When enabled, removes the columns that are not in the new data view. [[discover-sample-size]]`discover:sampleSize`:: Specifies the number of rows to display in the *Discover* table. @@ -314,7 +310,7 @@ does not have an effect when loading a saved search. When enabled, displays multi-fields in the expanded document view. [[discover-sort-defaultorder]]`discover:sort:defaultOrder`:: -The default sort direction for time-based index patterns. +The default sort direction for time-based data views. [[doctable-hidetimecolumn]]`doc_table:hideTimeColumn`:: Hides the "Time" column in *Discover* and in all saved searches on dashboards. @@ -391,8 +387,8 @@ A custom image to use in the footer of the PDF. ==== Rollup [horizontal] -[[rollups-enableindexpatterns]]`rollups:enableIndexPatterns`:: -Enables the creation of index patterns that capture rollup indices, which in +[[rollups-enabledataviews]]`rollups:enableDataViews`:: +Enables the creation of data views that capture rollup indices, which in turn enables visualizations based on rollup data. Refresh the page to apply the changes. @@ -408,7 +404,7 @@ to use when `courier:setRequestPreference` is set to "custom". [[courier-ignorefilteriffieldnotinindex]]`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. Useful when dashboards consist of visualizations from multiple -index patterns. +data views. [[courier-maxconcurrentshardrequests]]`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] diff --git a/docs/management/images/management-rollup-index-pattern.png b/docs/management/images/management-rollup-index-pattern.png deleted file mode 100644 index de7976e63f0503cfb861604f2b05b9335b9924a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59955 zcmbTd1yodP+cyr10t(U+Lpqdn4pP!XNhwGPNDMiENH-!SFm$Ifr1a3Obhp5eB1j5I zH~hEf9DSeXyx&^?Z+$ElYqMwey|21|*L7dMFrWX7#Mhr7#KH5 zac%&2j?=@!F)-H86lJ9}-7tU5-VEo|#^_pnXFE}C+l6n9RWqW2EtxS-3|0vRLX@`b8oTtLcwY6!f1FC!TpXVK@HHpj< z!ruKhGfB9aIrAUglt(8g|KXPq^zVOInY6UDzKi#sMF!M1%76JH_}iNPdM1{(1SY72hE?#t#KfsX^X*Jr;kY)ebd>d?Eq;A?xpN z5|e;FujOUZ2yd)!AMLbU2RBnl7@H6bZJ}2UIoP=F{k^Q;RCj++Upv$UtZ*ghd=lLi z)a-Vie(d2BwBUU#^Ute;jQ?Vk81&{hf50ovO~B1ybHkh!Z*CA~cN_aE{s z#b~`|V^7#ziI?;9Gw#+Wa7M_D-@46~gFB60lB|@l-J3giAMO00s~~oC_>E|Rvv@`~ z@=0&UKAotqrzOPY^U*BK@B^A=K=E0%>uDH2Na9tEi?i9daHNX0*IJtqm+B@{%9H{e z>$e~k-b}5{5Sq^ugm*mH#495?%?AZ)v)FHY#*A{dr#Zq%+#f=DhT@Ijl7^Avrmj5*5XP z4Y}v|jfbC~-}W0mg{jMrq+~ShKBKeIT%G6m2CC4ro`r+h>1G9uT;whefD;CpX+2@mOp;9_M#LOy}7|WHaQu*KzDIxF{heM8iyf( z@IU^bEoTC$D1GVOmI}#)t8deB_5L83kAhSX#Ni2{74E>0qEZl(5AcBN;_Itm4Vid4 z`71n_tgau|tHo9(P4V4v$Ls5b>zNh9i-W!$Yyz75Ox*;WoSc0Lr5DQ;+;mrW`iv(9lrRRc`9tIfWm>5UB+cuL1(pwu0G8=JzzFn-e& z_+_7{^YjxKruI0`*M#C3TgOjBBdv1E^B$`=FWbuvXM<;F#lENJ=C06)x(=n+yj;#V zDl9DOl!-kxRw=Rlq?TaDL{_O)a{zHYN*s6Hc~|Oe z&nzw|%v8fiNOhW%TyWOb0A=|)1Y_x~=o1yaa;ieyFI$+jqQZS9H_fmJd=)NQ(Pmcs;#*KqhmMJxp*byzs;H zSJ4~}ugk6DAWY*uMP=#*I%Jn(e4Vzjar7S6JTbp9%H8X7)vlxWOGsYMzjk?dSscP= z&Q0aEMEZ!CC9amj3jKqylLrd5%%jA9CJ$&<(oK(vTH9L?baxvu zB%?IuV|t=>F_>D_DvB*TfS+48f^0jYoeuT%#bj}V2_0vz|Kpt7H}Bo|;;<6-+oHmA zlIPT96U2&CQAm*bif4Rk&GQ}`ovZz27Oa)jct|{*5dQ2fb}rV|%wmNkS*sjqtjT7N>2xu7d4V`P%}i6{x`tl zP@1DOq`gUBPji2@u@sofrHLq zdxVqjg#9rLiE$D6`BR3N&{n83tE|sc8Tue&;H9V;V_sz>Vf7Q&YASP zt=T;jit4(<5OJTD6V${+R|R+}*_+W;M@-{V6H*tT?A;iySE6Q>+zGIyci9utJhWn; z=@lA+0+l&3)lDfN!`#f<9C2J@NB5U{8ut0kBm*AB=$T>4q-9L5WHT^5_i>iY081s& zp%v1|@E*T5pyi9u1&c9|C5FGnP|ykd5+oJC;3U=h$ejxny~^ZOCnlf4a=$W=_S=i) zD}@B!vdFAU{9|;2(b`e=GsobN1SOO85%;@XE^XB$#S!sb==cnfF1Q`o_ zzMmglqlg+2us&@i&rBHLgZ!n&T79WcE8MZ*AyhZy+3ye(pu8Hs?Y<~()!|a-x=`w-`h*(Jcs%GYYN|&spEtG z1vRW7MSAKf3cz?38}1zT+nC(*t`?ZlyM-?&(rOaTqwrHx`e9Gh!oc+7QZ}BodoOh; zS5xD)i?wik-X1IIq|2Mc6jdqYG2MJ%7(s`|AxZem6dq+Muy+A}9$SLh(;822b6keS zsYk|01`oTy4Q8G-70;NkEKX@`Q23~K^XA^gP9bN~2ijd6!|2Ljar5GsVcF3RJ(sS@ zP`Fv)+3bTHgNN2N&*%7=rM$qiTT(D&ZJPOhOp8&hO>rw7)m?s8=iCIk~=ML zTKX4ke1J2$qh9WxL?fckdY|9AluJt$->vd6a(D!ISz9dvM9qvu$Qn46zC5fSMf7b} zf$V_Po$F1snFo&jk~~COT1cNffC>d$myU+pOgSpDGT#E__x%EobOKy`jArd6iSodD zkGHM})mQFC#F0vwY&U3niU~E4u8NuFl=B> z;$n?IXLr*cInczcni6WR5rug0lvyP?7mL6+t?@pdr+y!l{dvg*=zNG58@sOp9zd*% z?p{5NEwc!EX|5)AI6F{lP+>l#)pqkgqSs%x*9#@SeVZZhq8sYbou;w5Z#J#){x+!d zd5Q?NKU-4^56ODDi$Tc%>y{rl%SOl?y{2RRR&&wWBl^7*cI?JwH&oppZV*MOLJpI~ z^DjcS+LPYfD zE7@$A*%rRcOR_0WOTy=6lmQ&C_zjs(C#$>o<*}pwNs!Mw@NyHwmN($>Sp8A_a^7x zGPpTmy@m(V?>JenjqUdch}b`vG37l@H$o_*#h`nUW8Lo zCX3u&87Zi@qfdhOz{ z(%#)W30(S%<|~e?oMjz#$zm{(8}f<{)KYp%pNpd~8A5dndDOZpCa`7uIcG8sj*d)X z=ion*9F}U}a}iebtrHWrqCfLe4do-K8)D7Cabp&PY@g?96z`ZP#l(E%Ftb?o+CwMi zzrJF=ck_RPTq}1t9s{#KDjF-v0c$%X3HnNE{H0q6JWiidT~&d$CY2K;4*2cLu0cGi zHfSQ^f@2YYFjU#%3oV^4>yXof_G~jsmrIGZi1`?ae7c}uRv@Nmw|I^9CL)U)bb5@) zvZvC3ybkerz4nvH$;!&Ar@wc;!L{XtBqFD{Smn*nSbxQk_Z1dHi%lO4ahjj%=mb2` zxq=}_NBO#%uj@A-V@YXH>5Ox?TrE3QAaYOo^_%n8*3lt7ni7t+qyY zMrK^w)_4ThYE@{le+@;ijpL|YH9X+K7WZ=Xn*FJx=NsVkV%Vy`KWe){aPMA>dM%2G z`Tb-oJ1X#=KT{Cbb5A@Q**lF*pkSzhMCN{o9d!?*7R2~+Ba|@zmR+!L&9gCgEa49_OH~81Cn=OgR9(m4e%NuN^Q)@|N8U?k z2*R=p3eaAtLhaIh4iMtRF8gBqBn}}9i*+-Y_xW6=lk>)iOdh60`DVim-{HpYfh0+c?!=pDIqWyuW;PfDa{gl&?R0$XR37#0`LrIk(mpm% zjc4jB;&-_FE31AT#B9=DOs%kjE}Pd=s`_TiDWB%#-Sjlyh`&gYe4)CZg@koG+`om+ zFRQj;J>SUS#m#!~4!-P;dp9CZvDEvW^C~Je9zL?-m57SwF*($-`4SuZ)a>KWq3yGq z1WVCKm1$*Vt5QD=$VdB4{%+`5x{?uQP2YfSEL+(|cU&^veWSsSBy_0JB#shJ>)Vpx_tq zLwtN6JmP%32%;Cg>}+=L;7%+3M!gC=tmxrc=ooM0#Psx?n|N$7uUmOzsho0Vlp%Ue z=IQQHKYmbO*G;|TaF0d28YaHY@f4*2HBaE%TRnLf@perE{oYLYNW^`kl5ccmgyF$> zS>V4jCPXGU4E!mA*!P#?qjKIz%9jT#i`HXj-{;i${uJWR&q(v3QL-TVV+$%zFgRs0b zq7io2(r5+lx;pn>+yH)F0zM$gl!KY~nV0dMY0`ZRhMrWofOjX{k=u<^sZAyzyS9a! z6^yg^);y!HaVqeRtO3OT224SaiC#HniC8uLF|u)qNrFbn+xv10@bmqV!b=&iv405d zn*j+nv2JK0cNhCt1IQk6DSONNPYx#P@5O2@S$gYyFFW>rYC>Z@ z(F%V2jMxmyo>tJ0PPlq!FO%QhvX!ec_;!iM1{!#*Zqi_&a9}PN8h4AD{hpq6x!eRu ze{vdoU(uY^C-qps`d5Y(9}ka}<)_i!id2+#_uIZ-=b`WZ&CQS9Zqk~886WXmyn0q5 z^^vnSR(=*$j-s+IwqQkXpOk_FR<=cY1o7I5vhwEjfaBM-1I9 zCfy&<6D=}}LhZeRfnU75^?(h;sn_^Kcw}sB$!!=&ek#eJ=tSBh5vI{}AVG0ESr5clz=~ts>k5tS>JWhQS7qX;N)lo5mw}&UmvR zw?P~^jqI5eM;d^!N#R(O#O;_dLoFe2(iP&UXHPO#wXuK7*4Tau4&yWEg_ijSi9Eu2 zGQl;&HJ<Z6}+C6Lci|Jy0Nmd;Tj%>w00{Dxc}K zZNxs(lr=ss5c1;geXgs+ni2b^{Wr|;ISmxiM40(VUHQX@1&h5$>pq^U?%$Z=vfk*DF)If zPo**Ag^s!%<@Z;D1?wP8I7d_6&h8%EjH0p6qJ~v)qymy;PVflFmDCoo&#_5J$_OTI zho}bh;6U1}dNUq~|0FYS5H6WcP**cU`coyp-B+1_fvMZeq5vyxI@#Ooy`?~MzXXmo z)aEbK3AAZWhGC>%N5yCFEI1skIUzhibb?XpLvf}CD8;% z9An0inB!b3$35)25xLd(T^Rh_C_cdBX?q-1)#Du?wso4b!8p4&tKDPwA@yi{aq@=pof@Hc*5IdEkY0>2$${3IFf0q9=C+}bP{X0 zrj`~>g;U$o4UN$peYKs$t%;_@=;xt`tK$>?PX0}(CvItrEj35E8P_f`sCc35p?T(1 z`4V%d3_M%w^Vt!Iuw(UT%Qc;2vpf*{pTZvoP%p+SCz657!sCp}zFfxnUasOUwZ0bg zggafdze~yi4L!a_?V8V?D9&>iA_`4NKyrp}bF#DRS`x_(y{gdOV;7t_ilAo0Fv=`^rwW{BjLbQ`5~ZC{c}Idc%=r@st8GJ~&)RCMe=n zi{>$Hlgz-#izC;uFO?26`a51P-yyh+?BvTw-rNl2JUC^9?AaD1#Kz_o<8sn@+V7j@ zmE8Uh(LH&gsxrszT(cqAHklr=5>IIYHbLsw*y-<_u9Yu!f;yN0=CTaizsI>QT<4c; zTWfYI3s%`*zxj!}Ey!9-2GY`=lx1h9YqsDNA74c$BJ*HT zJ~QaxtuSl@?{^&?!w*6Q;tkL1Un8LXh)kM?{4y{ze04*&T`g1N1;bt@2&(!XPy#P} zhbzY){R5p%n5|_3=Dai!>+a(&YYqhJen}YXl*!v&*P9Y*I(LbS-NvI$z?-hJJCiy@ zAi4L=%|1KLm(vaEla;E8cH;Pd1Q3Flp6B689-)ITNuZoPe%ad;>+TqLQH_D7CLHb* zyVwEckF{vX%u(vBY@D8EuP*B16S?e-ub=9IrYy1i5DrOXGFIyAl~Hn3O%t;+Q0+O( zO5`%IHY$%kR5UCp7IbNcMn8RRcIniV@-z3(#>FTn8lzQ0N}S}KNBE&ja4(cLS>%DO zRX)GeALI@OOCDV6$b~TVbi(z`9EbZ{em&r>AG=hrXr`7D()ahn zMT1N6%`T|kd&~U<(o44uPPH@vSobi|8O0`xwwg@jci%_}B#0lvZ$60Hwzr&S`y*yx zBag0q{t|xUCem)^hP)9jUZh@ykjXF9s(crJu6s4!S+JqbnN7?3V@iS8(0Ast-D)fr zY_uY~744w@;)RJp)f(WUYU~#@qL+Nk92BkkO~HL&5$BD{!)n2|!Fc?~=U2gl(M4g()vk*ajuhtlQ(dfn-S{{AIX6m>8qD`nk?7FKhP~>M2vdv@Y@qCd>A9+E zhC{W>mWRzkZf&tlCv@I-zwvMo`)VQi^TFNE^*QPH!}u_!{}>p6(pHt1SDzG~5egg! z%Zo783Ox`G9Uue|fd&AuG#%XQlDIa|mo32W?HwE$l%Fo^pLv%sr9kGNIQC_iHQk;5 z`$#JUD2!KT6c&AcRA$F`TzV}wGCcg1JGSy!>iKT{%O9rT>jcMXg25VsM7}Vg1&^Vn zcOLOy6KqD83$`t+N(%u{O{LenrSv`aUWgcEwCxy$nKpk2WNNG2;(c^pWC_^juYOOc z9DklSI2U6vus)A;RZ>d9jY%~6`d#GAxbD?rR^7s4o4k|mW*rH$DrEG7DRRWLbA5AS zJ|BIdeAh&G=S79X*!{7f-)wK+2vJs1aV7I_pW|t!Wv%5MJpqBqjydIx0T>n`Z$6bEU5p1uBI1$)JfybN&8aW;`nW$Bs?cPNy?#N&;*l_<795HAA)yqF zn1J*D-qbh17`9Ja*QEX5og{y~g-BBZSIM>rK_$$sXis^cEu9}@e$gl~wa@+_6gv5n z)flevioFAl=GQ!~m;!5U7Z(#8W3G!0K2D`_JSXTkCjDfIDLS`#EyOZspVWH+6j zqE#}?_`^gP;@|?QwQ=Hg`laTBp}PKI^2(ay7h(e0R5I)Rsz>h-A3uIP#I*a+m0#We zn>w6uj7mko%|3|1p-@7Ksk}5l=q8|yyof4Ej`vYQ`XWm$Z^VKZbXA+8gXUT1&6m%{ z$|C?cdT`Njb?tdU$r(wi2xpuQVBZ*cQ?h;_5oo01Hg-9i(R4c6p%ma(c^HoL%|NY9H2b1IB`4Oy|Z_2;qPQ;-Cavmdiuo&6C+odFedYYS++f z4Wk1sc@d6UAro(K?!ga4L(4l2z*jP|sAgzxz6ZA@eT#W2NJ#`l5wc%*u21YMQ{JYF zx`ME+omve?p>}iiwsUeeK%$Q~1mV=5m+4J5zfUP_9bOeeC2szLF(C_tx-TR*rBg+u^uQur9JET}2u_bfu|;|Kgz6 z=9e+5lM?|M=!rUwcZg*G@Adkb_sC3+e#}`=&o(;urQA_T7HO*mGCReB9<}cK=6EJZ z0Aqs7MW?bll*O-n-+h(|3e=`bFU)7<;0Wb`#C-b}6XgMJ%6FI;9-aaM!yRqU>$kE? z8_PYQLkkU3>u4S5~La7H<~j%W#)fnYjsJp%*N-z@#o##_1->fEUJ0`?;*Yg~s$?#nFc*5jHZBn9YQ^JfvAbK^h_ z&|1-e{!`L83X5sc{%PF0(g-jDftV1b=5vz8GXXD<=w}gk?1S%28w{FEmdtTCo_sv) zeM2`rQ?s+yp5G45vMW%o$)GjzUd&?ZjDJk4=c*)^(v>z?e-%4lbYJo{>l6nvu;3mw ztH9%3z%|jLe1aQKCp;Hn(2tC+Kc-+9mH#Mdm_iU!k`-DcT5$Dt{pCJ3LfL%0O42r} zv&K1Pg7r+C|IDSYZNW}eRot9J2# zCbA=7R*hP~O^3r2oLOIAs5jk}X4y# zmFtJy{`xr`4`?o~#;D)t1gamSIhI|}`#G@h5-uAX-aEof-e+5RCquuLEj_NGp9RI( zQtDZ^X`9SYpfET|oZqp1ruGC)^ffXQ|uGkGB~Tj>9qZZqvtC$C7} zx8Lo+&?@dMLRT#g;pfwqFaT1@yDCt0Waz&-L4uCB? zAG$h$kj3ldXLRF*|9AH1R+mZJ;vUD_soCl>!f_wz$t1|)*;%TREk{^MC`9} z|5)%$(Fe;zELg2Kqq{c^MZR1*UC6*QQ791`Wy#$?hYlw3N)gTPqEj1TKbCULW^w%`sdbBvQZ36z?#mxtHCc?Oz01cF0JUnzxA6>JzcXD*x z9|OT=;6CF`M4oC9M}J^8F}tz{L|f!hwfkvpd;-}Tqq{TMutKg>X?w;c}N*|kKz-h9X{(H-7i zW?xR@}Wp~jwSYX{VE z4cP*9U~g@KO&{y*;%0zI|I=qr zAyD7zf?88E(!1?vRXrF!k)xrdRUn{*J>INnRk@BB<*hmyc6wsFj+h{kFD$ec3eVHF z#g2Mxr~(i8r!AVt@?g5Ia;$gWu;V@kNAk#P5zct1%GirID1d9{xj7@FfG{qt>?@Tb zw(oF574qUYhYH^zew&sOvXBq?MW>@iaQ-nyLcgUxRXi+t)ja_Q@Pe~=VGux=U~282 zkqFb((i#)koVSdpzU1)={>kd@_?qz#AFK$&$0;r&6bK`esmTRVjC$RqoPNu?jH8=M z$n<$jv=C6>%uhX(U=Q+MO03O~Xg(;L-s*tO~jtmxQOgb|J)R!y`0UndUin^=5L(BRNIaTz;ru(eS8!!Fm`D^pg2-^-6STCdY(dhs6hieqzsD0?l#!bj2y?56{LOkoX@f~E zEiyR=A3!8>8bFqG(i!K&X1y^5Byrr1<)85lKU?#IYF#8L=+L{R9SYM81#qQYLpMPg zK7Q;SiiiP){R1>FO9hmwb4J+9M|kiaLabAmJD@uYEfNwu7=9<)>Y<2~T*Y4>nIO;q z9wvD&Fdd0Fvdc#-xe`!D)w!RD44DWsg~B9%kO!inb9@; z3h0Oc$lPEuXDXkgPi!c%Yw7e2CI)q6m)KAr>Pb1E+BSHK_<)`0KkQ3d#l4WQ5r|kK z236V42&ia~0(MFNi(p_k%v3AG1tLtTgzEpye>`ShV1@v*0KYa@2yh%D2F*$Q4aDUm z!4e|H7qrDD$mX5Na*YHSu#zM^4u%$iY@GYhfaGd^@5uK8N+rpH)13fBa+HrOzIyaa zh;55y?ZAx?#5Uz$3;Ip19qJ;@(sBSvs82d;X}MmN+(uB}> z^)H#&&TMx8_GSWX;)W6u(A+;`W))ET5X5NFh#dT2v$W+etMhVQdjL^df(re{?)L&` zY~>@*D&HaMOBzBD5nV{Y770N#kzHX^`I{oYca}tzTBdj7U~nSb0+D4*FG>Dr3N;&0li)YQce+_mZ&%fmAZ_+g=fY;$d}&zrG05HEf5r@yx#Z$Kd>Q zk;`onu!&zF_j^+9Q_+YW+({(;N&xb4QxEu{j2!gEne+GBtK5iYRrtZV#TcBrYtj_y zTjok2<|2!U5$G##3yl5!`h|v1+g65H&um#%RJ+>S3nl~OWDBB3b^_*Ze1LMd^8I!e ze2es&N~{m3fxy(`=g(*$SS&60nBohMOG|t54|Ttop^HH90dqF5W7*uF!AZ%;ym!BS zx8gT<-5klYyX@pMw3@44iZVtXZAqN04CpT!`O8R?a~hi8bN|*VdZ9_W`NnpxT87l@ z)29zi%GxKQ%LcKLQLn=5u$?-P<%V`8u^rYXT<$G{F z+I?X?*rk>UFRa-a{!;(sl|ue2AQ^M&RAq@CtZ&T9rwvDx0lie4Fm$pa!w6I<@?01d7R?NCD!P_a&3E>F3Jj!qJ*z%LdA2JCG^7pj+Y93}MfVsqR7(@Ix?o|4ZQ{F7CV5 z-7&BGsLKKFdE#5&-|uy4Qr$Nc&cHvP1WaGS z%QFc7h*oyq$E4W#wLd=2u6*!q@B|Z%jp@d;HznKYNZ738j~F z;O#asKt)&nU(JEJ8aL2unO}RLKqg^i(tIdGyze7?yh)yJ!Obnlw#zO3S>8Oa2^RR( zlKB^V-)oCdVL6`cwUexlzJBkuX&})k;xbh4%8n^U30 zWzN7)BBYQ#x>GZySG&}3AM`}<9<7K|dsTvhhL3m3W#?Dpg^QzOq?D=7^P(3B`4~ya zHwT}Bf>TzJ2%!}^3HLqQdl`?ttJPZG9I$$D@i)8mp!{_GvG{F{#Db;^%$HbHS8V3| z;AUKzEiZ|*>sWRBuf8(0DJ%DdHy-w|WF|#4UJf>NR;-WLr5YJ8T-|(Q%sUrJ+;7m} zl5Tw5&<02C-0&aVC1KHp*wH5QxeWB{I@ZM(^v}7ei_z+gW7tWj8#Em6T|-C0f|x!Y zea7zCMaURUSMU6oV7MMD_AI@`zW1O!aPlU5f(QpBXwV?_r7tdn?BhOYBZr*5O5@|E zN2ut3mAs?iW>1jJJx~I=$Ne$z0Gn?hf_&Uvk?uP{K;s0PED;aKFvmT-dFw-a3$95( zG*5U|02%4vXkf@v0PZuO?^MeB=38`w+I8IFyH(NzL{ja;XBJooiB7~H+J~t>efo4-6Wu*7@2PZ3YR2s8SSniO?7S-qZcfs5Y{jqE)q$8qa~PHP zj`NzT`q5p-OfFDwXn*oB-l?t7ky^P&Kqr0&FKqp)Y!Qjm0NegrlyasidN;k>N^$*u z#zL#4ip43<-9_sk+%5QYJ~}43pDNSV@0AAEpLsY2GoG_^6GTM7`dlxM~%jG(PKS9+)xZ~H~OMUIz$*-rQkzMAaFO8!3P(=4b?tV2h1CJwKwV@b6BL#ldd&iKiwc0Ov zty_4#&~EWJed(3$4~}INlkXH!lh1W!@3)!Vi3r_#O!TppIck9z8193o^2atu z4>Ulk*Ni4Z!k3|p_Q?UCQtM@W7Y+j>XqR$u=5DE4ASdYGts*^8Bk8B;HInXTxU2`W zDOqir5o384=0GB%R`GqxLtvP9@e%JELT;aM%5X$~sUV7|-0X%Rg1rqM^o1G>A|iPB zR@6cY()*t9q-!o#uqP^L6Wa_q=XrFCea6^l&M4EggZ@8&Q$R!gcPhFB66qc`t!-V*ftc=wY~I-=JO2W3E-qJ;nSM!rk-2| z4|oDhF{U|OCrS1_3yLwpaQ(r-#>uH)XRX5C4V|k$p00J+M}24K)$D%={W8Z7>*OuT zN}uAvvzLUJQ1U-xR+&LptWvb)sbzBN!S$VYM`04D=~h#@1NwDrgwNPiYtK1aT<5** z$JZQ=iV_nMR){=e@K53idaxfP?)C2b-ts-7!;6Yb?8f_BA^J7e1#J;4*SZ=jSF@0h zE0qi+DPat-rwWAa@>uNX$~PS(35ZvD%qkhsSyb~SHq(O9DS}WMMMU=@24RTx-%Xf+ zZ7{F~z`*S^Mi6T_`~ZgRilnnpguKSoOq(ZSWi$QD*|E(JbgQ`bnzP zb#0u?A-G-V%?WSz+3UP-%veib2TK;pdNd^&LsjQ$b;-1!yeMIxDsym@ggmId$E=j; zbF+s7x^wYTxj(EfYCb0xF)USL(6H@#6vo{waQ_~72aCMc>s0AE45{E2DtYv69m?To zq4Bi8%4s+qrhC>L9gR>oenRJ-&;X&G!w&3|=p*K9dt@@MbB%NV-WTF-Xle6NE_p^5 zR5|M_`Vl&$(&Ukl*xhHLw|*=nd-+1i_lKy!h$>S18NlNWAk)*xKXa?xng}vloUPlx z(!Fmj37M{0Sa2GcGCBpieNaTDip}tLX8fr4c8?nGV-9GRi~@5_2)p zI99qF>{J?;%iT-+HCtan5&|n^{l;>cd|2w4lk(|P8x&@oHX@o zwiH-EG=Nu`T{MrcdLKds+n}#CQ-qk*z8Dn($JO51j-#BJ_dZJvKGP@&hX?4GOOtf8 znNVtU!kb>*;cvZWj78|!``oIgoeSq~;rTHDV4!tpaR&@s$wET=(1*nAQA@;K3G?m# z2?#keU<7XnrGJXVsX{{e3KSu1FaO0ABjB#x*+9BcWIv@1t!8+OsN=OYnPU;P66jI| zc|o`CR~g^-Bm-ec$(Llv>_-k3^A~~TpZM?ZDF(v8^f4rJ$^5R5kNPvOMmy`sDSM$@ zbP34HqSSA-fXpFNv zjlLCm$N0va5FQ=xbm>^Nx5cwJoQnSZMI3Q7S=7ndZ_u%b+@T~G8&>Bqh4W&Z*SDHG-B8Tn z&XEGgnmE3IccZdZZn62)QHaM^C->(S-=|}uS?O_kALZU-Zy%SU?1_PoS7=MPD%K8! zaKE9pj&MRnk96}r;@Z_AsUgy4(>q$pFhB^0*j&Y#yBygO6sNy?iA_)AwqL zaRH!IgC0d>&liLEm+WPwfKETjPSnUMKrj$EZ1h2azP4g9QXP5bC5oK-dS*v74tB>B z3*c*@;Z!zqSrp3wO4=4PN9o&ceZ5d%u$ISJM>%&8k*Xz}h}fQQvXIN#C-1M?)wFq1 zDV*VMWogMHl*!|u)JS#r-DNA9=gl?Ci{)Ok|OAt;hT29Q&i64SuYSmh$~seHeRi$6MrHS3z~iU4u*7_=F5(K zm7dlOErL2;06j$8Z2DcjP!~lWC!matAE#9~nS!GxC`gy{%sNCa?g_-?+c)RV2++>t zsb{Lo$ZpTGTzUC^nn%qx$MajFp(qXzI`yaJZMGM*;yy1v{fcH3AjJP-B?iY;)X zf&EbKFJRj*nVFK| zvfb=0tsi9MpWGs)i>P24*!{R+eJ$OJ1K`G|G7!4EKZOYMCWQEzUt&;?0N_)B!cX*h za)y0B+2jH(#}vE?n65l;Twb$+%5V1<*!!HH ze^bDYUX-w)SGy&-K$w<_6YCQ3sR%j2-Vm2wV^MX<;B1tcBd!^^ zxAF8YTBXt$h>~|Jvj0D}zB(Z4Z~GPnL`gwPh90^>7^I~}x;q61K^z)HTIm|P6=~@j zq`OO$hC#Yhy6b(o-+S+S@An>m7%(%;oU`}ZYp;FwKK9{E)07e}=hn1EwBel5nNb?* z?i7fhQAM39g0AIddg)bFu7CY|IUYSKDQy-P9>~M;d_h{X$#@SNv51eXOXDF?E`9&g zFSE)!CQC79p544X6r25x12 z;zCCY2=^EyK@P8HhCb@{!LmnyW7>*`G7x&DNW$ueUA@px!p{VqfZPGTQvGpDEND8} zHm0h(@Pz`UA?s&2F$@21xsN`}2S0U6jg~J-q7i`95Pf*Ne9PNb_ST4AcPKUs8^bFs zy=&JK$mPxtcrE5NC+G91 z>wBP>4=#MuK9wBHhzfyH2(oa6kfVB6Yp~@Y0JTN4Wt=JZ0@~p7t=(z?n9r{`c)>gx zfa88_lu`Tb51jvZ9}!;ey23EURkf z-36(!*p1YX09{N6-Nzw-Y}@FmyT9G~fdcr_?Q;5`+v9a#>&Zpmd2&&w$-mxF)Do=u zpGoxA_f)>|#W`K{SCJLJH=mBE`?)jY9&YS2Z`7vyY5w-_Zx-ob#JdEn*nx}r;dYZo znC0T{oS|w3sdxIaKXA@D8w9X|eZR;YV){S!wI&nKDlBBW)O+c*@=?-lrRDC%FNz{D z-!zh1JT^CUa+izvf9&;7WzpW>%Z65p#@JS;0hv~p5 zs*vzw?#KO5)o-GG)o>Yq<(Da+p2);4`A^&@a0VJ$i@(wubd(MlLJ=TAD``mBPm*nr z`*aVGi$}nTBTrZS=9Q9P=B6MCM{P%=-KYCQM@L0Mo-I91X-lkr+&`dRF_Zt0bevGTwcxBOpIE%tKL{?a#$5Lh{KSUCi6Kk4m zxFwdX5Ot@>*q7p`6Mcp>sF`s!O%JdFQ43gmWc&+Mer@fJ@AL-&-@3gd|KLc{rJ{Ys zhH6qy6GG*g#1fHRA@zl#$z26;MyJb!5#8I@GTSVnE-10#Z=-(0ug&Rfi&o;=^E}41 z#Mce;3O+c<{7rIf^Rf=t@kB&@PoN=U*>KyeeJ8U>Aj>@$g^?`&2s^c^A^HM=&$sM; zE*T)RnmU-yzWXj_!Oi`j>=D}ta!JG&hxqy~-VoJWK%j^J%ZG=n>wOtPX=&+`sTTn$ znW^$Ft85FPlxt0`zkht6o^fz-4XQy30Y!mB`sH2^a9HitC1;D-wM6w({}u}?p!HGo z*Eg|X4$F9aK5g$))H>IivKj>WFzmCQxSkHvscOyNrsNGERd_Z4eVd_EV)Dff{=)vq zbI0&T&6akNa zJd3k?e`fXGBckU`dT1a)jz&3qB|Bn`nhQKTT6Y`P6zO)vo$GYFgVPLG1k{0iV z;>L#qD{r+)F3S06nu4RKjcY${-c5<%FpEXb+}ycJoZri;0V#{^E~i~yyf4x^s-@i= zKkSlE{KN@zb$US4k!Mr*EtR6FjIZc)hx}M8>Al$0tVYq5tr!@93zJ*&Uil@#Jz%}j zO3WZFN11=<++PLKp(zcVg%$l+Uv6^2l=KjuewO_8YgR)yK6QP+MUiQ}OK9m!hc~#` zDhnTCM+sl9&$`WLN0;C#Z#4ItykVvs#(fn}p>O=!- zLc)7(C#7=vk6WXN)n7lGNj<~6@kF;+L@YcWc=+j7LM}TQr-86g!92MiR1wxDo> zq`{l+cHkvYEHSWb>S$Q@zy1yEYvI93piC@+-|*(|-d!EO$viAu;$pPro%7AODnx++ zc>Mj#%FsJn@c!%L{QEodkR?Tw#CdiL(dfo?VEC^D8PSoo!(7GmbJb?A3(q9+i+~&J z_0JO5W(=hC+rNmA>KWo@%5 zU+0y@J#N2zRVY~~d{z87$G(#CL&?Rgq8jpXG$JSc#y=?7R*oB%U2fico@98q(YxiV zC+|?H1R3!h5P<+ELeV{gT#9`&WILakYp=WDAj69sz2r`XL~?%Vb%iLiC&I^)5axB{ z2+EnZIkbYJXx8|O2JJ*@hF4`gedUwM%7t$D2WaPMw$ zUZ~4rTUSTcm8&?4z{rE!+nc&?tye713aP2|>``@5A*gQad~d3)qh{8JySQwtTjDCb z#rN-pkPrUDQ{?)fDSbCN_v(}E28qZR#0EB7hiBgRUzxr|I%QYDUMokGBUS&_PGOmG%|ZIzYKtq*VB_{*EVo^XHrs{CfF%0&eId+hMFbL*!(FRo zqueaiR}zA=>SPZb8`)?$eKm7^ug<;%xw}+1U3N=ckNP^|z_p;d%IG%@m{au z5y9AEe(`ki44=cYOKr!nXuNv8IN1k}hG7BQ`gEFt)Z*~!tm)5oxQ6IK1eFpeJsH~l z^3LV;9^wPWM1&JPkK>4OmYe&a2blYvoeV{^6K^RZjzQ49fq^9SFteTzd)P->u*>rI z5_|z{4@UbQqkQ%2n+ZdaA3h6Br-mn_2gIKH@7;sdejk)01{O(Jdt*^#y!DGqP+lQai<71TL(4NcGaEd@zlZP31vmn^Iw3JEo@b8?bKo;c@0aBAdpB4<6%lFl&-L+FD{Qt|7l0Ic`qJEl-R9H2dK%b z%O*|CaVf`DY-^|a#^MEqOl_+r0SMDJ<*TT!o%AEO;;)0VUu5Rb|Gd{JGnxNbJo7dy zhyOap$!7YYWHn%ZDGSq44Z5bDl?P8RT2|X5QaFO)u)u+w7x2`#g*NGFuPNC1jL-vk zhC3Xe{ORTL52xNRC~w8Y=iTn?)3k|Xlx{q`t#O^{kNjSxyF3-G72Xa-Yk%#EtBoQ_FTLD*0np+)IM`wJF??P$lD zyP}~aOn{;{uN7f+s9*VO%Mj+RXG+qnUn4+RmowKI^ej*{0LL^>=7|$k!5PaXw>m}; zq0vI~#^P2YR7|(nX#1?u%OcEJea{U0hG)o1ezf?Z9t5g&gbvHD#UN37oKy`C_qfwb zBr3CJdDYQ?o?9GarCh`F2Bh?QoYZ7Z2`lC{zg}xxr;E$vEB>FSLQ%Oqc+RgEDt;-O z<1ClGXLu>5|5iq22x{MYGh)F=n?*k!MJeP|ylyuB8L$tLzTQaUuD2Gix|DU^BV**4 zJD?t(&4iRPV$OOwlz3SlaXY(1L}-Y-eq zZBIa-3b1(Zl6263jn?iy4*wRGY?Ob;O`or5sy@0z%2fQ@rnsV>9we}peNkT_95_#! zzFKx~75`#7vG8MyHbzkByD`>#A`%f1A%1!u$M&VhtLQlNUk^bNvI|`aI3&Y!4yPu+ z(Q(MQDyXE{`&uP&y&?OOOhe)sgtIljEr@&(y`25aykLFs!Wvd{YU>!aS!-g2LYFEQ z-*x_3;f)(Em-p*i+S4zeIhQ}fz{9lhGY^8f%mQ4(LTC}H7pnr9G;=wZ&rcQ8Kj}48E{O_&n!KMTSGWwbIVwNtYK3b!-uq-j&6kFquSaDv2OPG?$NdrEO zc%i0DP<|dxj4ze)V_bS{y42oNmK&{Sg%c#xPsJua$el4&BplRCF1&cV6?0Xhg5U8$ zAo=^2Z$4YU*lYVCiBvKVY8|S_Ts*t&4=g|R(q_8`{noWXdLd&{qn;N1T9j2%+TstG z;#fS;B(j{M2$eb56$K%=gGbyI8J1nXD4UDmzfPhW_{g(nt} zXg_3Ucqmw;V=CBQnJ|x7op^;=RVJL`+to2TB1?x%{<4K5OEHIzVyQ=4qnOUZ4{l*! z!>+U2cKkHW9`piZnD=sb>P8yS&C!KF_ejQT%C&tAnW#9HebM72^p+Iw3V@%h-2%rpon^P|x zs3LwV8qP1^sv?qb=p4`o6)%$&S=g~0u;Q?O-9T7iev*oJ{-m&$@3a&TRMVR@G^a~g z8Wm+sgV5*;(>2o!eOG4G$fXEDYRj@re``J}#oEd{r`8o*!nB_WnOopzV*nu#RE6nM z?AxZRVZ^y1e3!lFB@}V~-q(gxPX)QK+^t=b#P6yEP)$~4k%n%V-iGJGyA-4m^FqaWbcE42i)y`If#h{4yKGAUEBw8_O_(})`WJ^0j@>#a! z=MpxoRVKLT^S7}qge=Y$KYi9kXfPW;X-`Xj$wNPD*&N27!X#fM{;oW$N(^4W$_k4A!jTSV`2J8+b@}tl#y?nv zG3n%e{iw2K`Vd@Lp4k4va^$WQmn+3!aSVb-pjs78G6;@K@L86&o}tAx8QwX+Ye)=v zh%GGT!`M@vPv^+EV`|mk2mNM#o)P4VpMvxp)9MX}TeIN!=|MRf-86ktw%oRXoqg@I9~GtEQ>V>g4&Gk9 z!H$j?Ik|~~Vi4;D`x07McGLxUGk(7k_Q^!CQ--u~#_L>B&p?(e*tO`DfQToK@7~9|uX- z^&JAjMAM~C(z_zdZDRbMIjK*0Ae;c|jOtsjVA6s@7F8fjF)v_%YzlYhys;>(x_*%Q z=OHM1f*3g21Rr9cX?iA@DgPd0tvFlyH&QIc$R%^czwSfwcNQS;Ninew6%~0toJXtB zGv*^14E*OFCc>;j+w+U$`Vg8+o%u(9;t<#LvWFxbNHmDni>%uzk|ZDB1fYJ(b;nGA z1h=y*(T4fuwM!)`OS4onQQ$k`Je#l*n1K0Lov4c{;DE|)K zIWAmeO;74KoOn@w`3U8FcW7W5qoSS1I1VJZQFvmU8HvBJZ)O7C{?Pq(o*A zJ4Fg6O%a}X3* zK{*D1S7(7k3fnOu=3mz)obLTPtPkqbL6uJu=55}VV=*XY3JFVF&HLt)S)YUwfw%AqeszdpaxFke z4ZJ+OG5ZRN^lyXy#V`IzUY3`a|6!ss^y+j#!h-Oi1s$NQTf?6^bj`Zmfe(x0pjL0_ zyh8SjY3tD8XkM}(>l}>w>PToo36niN>*>C2r2Ch6hHq3_^VQTS0nSbpreN|NgoXVV zNU>aJ9?^%zAd1nY0V@anYL_iLhk?zrVt+rhVz_-&uiKVF3D7t?6@FrErfXqX&Wxc7 zIS>2e=7*D#?Tfghj4tEZFJ<%zo4;TLJGwsjTT#2#6YQ(z*RPTJyMZ$VGi#0Dn5vzH zVnUcOR!#Q`mYb=*=vQsdKp%DZLkx#)AV<(88_v==tTgncqY0OW~ifFEty3fD3-G!Q;8Y=cSUxi$iR%bJS@P zGY;#PPk9(Jz^m}F&xb8~p112J!gClCbzY0h1Q+^A7M4BEQ5dE8+U|7ksiu@xzaqDx z)PKe~=Gp&^zC?Ze4|kpOpY&%Y!m{6!{D)Hp3`l}9V1pZx@Eq%>wqcEXVN25SRCUry zq0tEQ6Gz%fif`!ZUP=c5=E+V2waGoypSQjjfPk};24|p&_!?gg?BIB3USX$c9vYk7 z52Ehxf2&MMYBF9<^M%xkg7Y2wS@c>ySn?Kks|^`?B?? z(IInRCOH#`)x@uBWkqO!Vc%9R50;&8-kE|Yj*`2gczN+?;sK4&jHWSauY60(VbP?@ zu2(8){P-Q* ztAyGBjs3{LACY6Gvd)reD};LdSy)Jric;LmCByG#V(aEgkf^N_|;OhjMksO zRu}(vsLqq1`y&uQkzeB9yMXF-3*FS*{M8Gc?m7)0B!jA!V@mAvZM@#e1FUrM^1PIG z{I%@Ct|P)!5;Rww=6!gx&L#90zk1+myn>f#k-o0~ELRAOiUV z&Y_gcu9b;`$QRo}zdJcgGH1akxhh-zNE!*004m?>!_Y2JXNw6Glw#EaQta6(}K0JGl<&oW&o$-KQb$meQZS66|s zdCjJ|i7mLqizl37(giwV|4s4HCtOXP7eL7g#DC%d4tl~ZA2<+hB1}>BpWTrTmI5=1 zy<$obdWJvc?|B{`qIzuQcv1OAy!A|=C%xfw9|7GG&z>bS%7Ymsi#dso%I~}nDS?QJ zH?bp4PETq+zkE9MP|F>}inYL0rqV{~N}bPv7$TfLV|X#0zz8ZaX=)Y@-@aj&Z6%6H z<~NQ*^jifZzEtbfJWDHG;_9WX{Iw`oL3CR7V%JKrFaIkX#{4pc5Qm9M5wg_|4gFSB znLzlehENbkchIF$6%wX+N&y7R$v;e3&)po$lI53t$>I<&m>yNhh5qB~OAE|en-gKE zhd+tTUZ7RIBW`CYr5KJ+#*Re?xJyF?qiN+ip)z`z)J z+)*e}HyE(~*e&>v{C~TmokPaf(R0C>R~tzr>ZKRJ$Nu*5ux^W1r2wMye&H#YR3(EB zH@99yr>(=G=TYM)aRMkDeT(wnBRzbhJw!D zoP3F5sYd(IUZo-Soj{=OD_jQiA&1$zV8Kdm0&E5l<5Bz*ov^|`-3*{v+W!!+`#MMx z{1|;97SRfXVY@!)`tNjAWQ^>8378TR1}4>_0*eYlTx-%BeN)#R zKn|{KibLG)&f6E_XNfkqvFry8zR|er^`ysn)_ePY<&X=|M{vxGF|Tc_lN&v9ZGpyvy2eXNdK?)jTShq+FE_2Kn#||!->$=C zAv90R=^`bjs!KKFrUX!r?dg%^ zyDMHtM{tzW)tgZEsw_i1dcEXMn1+u!2IYt|Q0Qk~{XyrKqf1`&i6S@{cCnK|{yEvj z&r-*i+n`a4jQCy#%C3W^P-2xe3)6yl^+WtR7?ykqXzx)Bdr>D(&jZyHCN?7mcrJ2M z{66W{vC%y}-sSvK1FvkU^y+f3VL{?!Vn;>_&dJSy71Fo^+oPgXaT{-&uV`X^^>8W~ zrQ2FGePi}oYsHbf(yOANQbel%R~-D`1*bmfgJS&#^)D^(>Gq-_)`sO`@2SLJoI?37 zd+dt3_zce1Kn2D51SYOfia+uqjE`@KByZPP5i+8CQftz`J^J>hM2!XD1b=XPHykbd zptZFwlu@ZAQNnqq$e6~cFM{?*-cCR!v%fvW*vZTtBl=YM3Q(yxx`SvTIV|jBqxLO=iyY`z4CAY}?bpd*=5Zq@DgAu%jFHxJBJ7N33l2ipw zDnWRh5{#l?VaLnC7yi8m7p#3atNsMy97sB*Gpx6oJq?1PfIV)cglQdM`5x=y9hFk1 zW*Hs7EEXm*0?z!_-sel=rYW1Dn)oj|dS3i)X28y%pE#$KvBBSNfAmE%2L6)QVWCyB zNRf!k6$p&_EbqsGw#*WA?gitoHP%Vax2#L)xl~TNV_QB_pSP2O^S7u^~fOE|`Jizb7mJFtAA+i09bCvWr3=#Imo; zqgWpqx+ErjdZWk2;P8(_%20;FC{A8Ome7AOfQ+C0+l^shDUWNd=cH`w6On0qNEYHz zKkiTVEl1+U8ELE4SD8VS9VepI)S5D;aLsC4PvbmdmAAX~2)w*W;4+s%M?g1G-(DqQ zFN<36GV+|9B{<~&BLPswPQhm3$aJ){0=G!fCBR=7^82#E>0fh$h+`4hrg<6o(^aM| zW&=R^%vzlPv3rRYMY{hoW=!xVAf}&GcFK?FcaDjFd~(cGr|4*sSb0x10G0X)g`_?p z;dP|s-Tpvk+@80unrZL}$a+VM1WQ>{rnOVqT|B*3A*Dkrn$q}SQ;W^dlr|Ebl-Tdk z1rMQ}WK9LPzXk+2FfxXPRw28tw(ZVyWtiEO6nxL~>0tZ ze&BcODF0ym?GMZvD&SXC?1>&|)P-6#Yyqm37cDTs58nK2lgRIlQpgxpN=aYhF6FO} z(immb6a~0wVh>PlLm#}6a!OCGCZBf%W%6ulg$B2{{xsP_H>Ne^%UFU zQR89%CW}Df*#}JumW@Nk^&iLYWN;ezh_yFxHwRYgf^W$ z?r*ody&(`SVj) zYH?^Kq6e>gHw#ZY(1`q`;r7bcE-t3PlhILaGck8S2XiDpv`EGxl=|~-p{{@OgFuMP zo1smP$!s1k)O?ft4g^6V0H5-u-1?!#poM9%;jHJ%UF(EYuj>PJ1e;oeHYLd)Pf$NS z;>*x-5!UX z-mODqC>9Qs2>=#`Ez$1=^i2cFS}zmm=-MBcYePfQE*%S}O?csW-rx?e&ycM*{-+p@ zjl*0%cC@TxwLV_v=6rT$OP4zW69oh{JbOGoj8vFbZ0V~f0+F@T|0HBUnh<+`g5e$_ zfV4qK?pM`NMHkM`#?r*t80fX&q!@P9ayB{n7~#52X+Uv|!^ik)?e{p=TRFpMg|b8ZzQi z{5YhVN1NB|h6AHXY6YG7I6P|)m*Zt#7gMpn|KY15>(d+;Nk~@pk_BF+Yw^?RBFBq+ z;FeR>Z%H153u~7Kh;2u_E3j+|m+j{m0w<9;|)1f_7CBa{6Pth}E(Dh#Yes`^+!m4C6*QT@WN+QvR5aQgdxmK}Bjv!C=bMr z1s=fVRUgO&$AIY+v;tZIz({g-1(afI>N^$P)$|CQrQy3W1kVfU~y)pCcKHs)9 z-siebb)wsUeU%zgNN+C*Y^P6OE8*f|CvK}|SI2=Ezs4>uNaWLAOHzPZ=5>(7 z=M^no=IPu@xjIJnhx0HmhD(QFUp>EmKgnx4ZKcv;w%}}T93qof*_q+RR2Ge6N8UV2 zV;x!_X>lZ*Fp|u_M~FGqWTF**x{kM2EWqP^Q$5rGBN5>XZ9cwvPl+T9&;_{?c$&+~ zvp<#_x+~u@0aEk?Q0x%lD$~G+$de$x)P1vUK4sgb@vc^fiBX4K5nUUVA1=>pWmd?Y z9;-6wO6e7Pn_0(>*Q0Vi`x=}_QKCN0kY*PXXIXw*(E3yUaa2! zOAP@iDw9?%6+w8vqi_ZmWau?|5A~_HNFFt6bg7D8@YfjpM_`$xM{Owb1i<;l#L{43 zn*TU4@ZVYB6sHe|O?!}lX;S--^W)Jylg;%z7cZ}eZV&A~4G)WXQ4Kg9vD?%6qg@^D ztPq(7-2Gy)epa&Zfs$=^TXTal_gh)Z0p!nFqYJ4In%bRl12mc}t;4rW#hCfHy|n@iCG?KTEwOrFkr>VLAkvNk6_OBVkDYlQ^5Lv(*?Y5t}^EYz4Fe-biuQ*4wV zHp3qY4?Q$z#;xPIt9fV@t;h#4_sp6h;94WM9j}^I*k{Wi2AR>Pj4ja(l{{%%FIxVl zF!G*DSnW|KGsw5|pxxD1{--s%diSqTiQCa!NJP=b$%mg>zd!swl6f>C61elVzoxlN zA63o!d0o%F_aWM+azeD$Pme(l>aI+@PY_6QRLZvdXY$L(AOyW03w$hc8mFKj9-Y(p ze#%>GqqPK_|NTTU;S-?S9C#9Isy4_ur@WJXK1`)MzXnD67$o_}EL&?pFJF^fsqX## ztkVF5aM~|oTu|KC5_! z5S;%Ojn53DP|96zF8qSt4oy>eJ@xp}WX`*;gh{(&HEB0u#5JK>_sCiX$%1q?n+8zmxCEUm$g6*>-L_``{>%m7#1h za@KUOO-tR}Sua~(3Gy^Mx8Sr>IE0DMu5q)P@5e|*Ju0vOZ2MjDZX}`MTm0PQPh_a3 zFCUpw3VbEe)N@vOTOTM0J0bP;qgHeP;mR82eI}u06?^M3?_W+B?P^=uZ40eDwe1ch zGuFu%EGK*nvaMLf$NAZm?sMt>U=kNtpda#68^BCBy^miq+*SngCg(pE;db6)QC7FM z+%88+BrL`seUqN_B-b^x@HZ08^|f!hvAa{{VyG^sN4jxj)1I~6+K42_S;N>j?YeUC3k+XFpZe)3CB zm-Z)^)3~6|OzF=#NfEeO$kxmA?d4n^VjzLlif^i=cb z8@#^dCn)U8KeH%AR`kk!CFB+L)q0Y~ zy!F>iJfExGGtu3jZK5xG+||U-7qUH^_G*tz%(4&WD0clhf&&n5>Y^F1*vp!I`U|Xf zk$;*muIC(LC!H-9&JV9a;G>`&W!;ny7jFPY41quv%GImClFc1TT*OzVHVzX= zT(?$MREhQ;*HE zIno0VOnCRN4Y*QFxw`9`{`%y+1CU7Jzpa!|j)xYAdqnQ%2LsQa4h9|o7yvK|0QjPX z^K)K40eK${3i=a(T!m+WW3yPM?>`7)-&sxmYrw($6$<$K^cD_M9f4WqrTrAoLkaa7 zt1_9MZvIGf3%kf<-{$0{G^j8i-}AjbTKhvIeze0rMVt9->C>K9d%0QmMvHS2bz57$ zbWbS*8Q9eDY5&6fPh*vW4u5LzYoXf}Db;5umCoy2(?0vd>-@e~X;HW5M=b*HCrBuM z22g)&00uCc|Ge9@nvVwkJ(IXqUT!OCQJw$X`0xQP1wZ`iG%XX~x#mq<)+tqeREzwXa_OvWqxsY z2Vju(M5VJ7&D%d6PoB{=@yv5xrv`eVL!jIZPp(36LBxP|8F(Xgi+?fIUctm@9KW#RK`w#=m5CYj#R`(y2$ubbLh|EerZ3b5HCe8!~oy$@vH{JY-c z&i}W^@eXM9rgb8`I@Z$2b&zvWwS=W<+1FNhU&OmJo^`atFxp~epv>pZAX#MN#3Q#0 z4y7;Raj%C~mQ|a#Myi;xHq^b__}-rI)#1`AM(O~E_C)99fcCH3Gl^aD2i>2k+AwUV z$}9SUgpEN?nBfosoKK&(rV2pI@?Y<&HhTx&M&Z6M+rJuha9@q25_z(A#NCWH@GbP4 zi2Y~L(nBUUU`BSPQ`@6S4;&JRJu88#zr?#k{^k)SQpdv-Dbs{7#8!)d&FNdE(y5R; zjWMDwXg9e<1n>XxhdDSL@#)BdZN_KapY1{9h!8kWv_4Y?Rfk) z0rsaK>BpfgDcD88MGdM-3YjENFR72&3;X=DKCb;XjsH;lVb+5w^4UTD^^#(lw2zjv zT!9WbGf_a$MnGxHfr_0Yxv6OdVKy1&_X{>+_F}^t$&+ltp^a66m_u5vSH<1aMWtoJ zl+Wi~e}musJbvav#A?kta!KQT&FB}q5A<<8FT@RzIu6+|FDabCcMxBd6jaD`CcC;^5%<~&t_$_)6anK|%%V3<<>QD7 z>-{Fzs}Zg9Rmt2WK|bq*5$sE&_zvbYZ{sP-I=ZZ^ojzGwlK#7!vLXj42ks!sn0KAgwptHeD>A$ zGgbzkr<0ZOU};Ba>hYrQvqr9V&i;!yoISh~9>)=_Dn=p~G$j5~Y+8iN6X6!!0P@7A zl}p2+wM*+YY@@!l;ZM}6I2IEB{Iwt&2qs=;^wVWml4;+T^B+Gp&z4*6Qd?Ngx>&;U zBiDYU0(TX}eR*KrNshr9jQmpW&HuU(`o_btTVE^y-w{?|B6XNS^paV-g~_?cIagJI zgeRfDdJDhc=ZyAyzVfUm0_vE7@>)L^RRio+&A}^72Kk~m%+vR;5d4&;>bv|xwsBbk%#Xw_DTG_i?|8H1b)W;i*7N`%)GCASp2o^3Y> z@Oc#Bben_Ar+WW~-TqLk7=C_E6M`5*%5;nJI1H(!rP@!`%C9m6kHoy-Cfej_A1`vd zYvAbts} z42*J$CJ2vBmD~6oP zx~VEp)zg(&YYt*AWv|95%SF;TxWKZEWiDPwC!1W-Iaz`gOVU*SFGFy)palnn@BcuD ze{4F z%n+niA0RcTNujzYj;{4MM^M9_1r57I27uE)^_@^_il8WRL?qi_aNxdN-A}ETA7*6a zpY1n>x}alxZ*W)Es2GmT9msB7`;|sxXBHD{V;RknqXOZxex44qH2{f0T`S8+cK~GeO6lT56d)RH`Tm^IeG~YjDB;p%G~$-poWEd1(=3^4%!=A-I$<8sJg?uCUT&MzwlL%jfS9p-dudiNB` ztZV+=di^jnj?r+iPu4s0xqJ(W@69X|325By{mRK(ijXd7fcpI9Q?h<8-|Zs>#mKXbrN6N9F`&5vS%xU)k=*D^2mI2OFi7M8F`gHJ z4Z3kiIc!ohxd=hIr0@Mw7yq{T2lGLhRN-UMbJK1cv0`D zJz9wzQoyzGEdof77v1LBq@Vu_gg-G)f3^46F>Gi1uAc2;2Lpp~tlED9UlDj_Yc7zf zILlM&0;EKhQJEsQu%^uwkM6?zk(A7=w_AM_ZI&}HUpT~L?iy$tUHxf(IV#EV8mMBT99*gQzIdz+)WcSv+T^eq~pg(dr{_J+*V zqmuu&N8(mKP-EHvLVs4vFrCP0^h3$?l2FFwlkh76cuU|jT6vZ$;wOMlDLcTX6n(m% zd|2u%(2!&jI8Ehswi|vN%WT`~usYVBnMEX-$j}K*Dutuu+|&zCiD_s@r3O6Ke%3u) zA~L-U&Hwdud4?&f@N1*!kV}0O<)3oLf&AfTeh)gJj;W-9!gL_n@!rmW(9w32dY4t% z@AbefCk}^W&zD}h78dnqzz_f+@P`FLTxhn#PbU59N+;%6>jpi=SwGDN@UwM@;07 zki*=zET97I#A6)W3b+gg5dz^7NEz*>AjLxHWHbUoYQ6mSRUU_nKX&wiBXe=yH+P|L zjal%V2DOCZWc!vYA*J5n!?%eEKO&AuLvvgPDPL*TX(n_KhMIT+#D@M`#d{W1zGY^l zGx$brzyauYAxsfPg}84}_IyRW&_SyIT`CNjRFxp$bght4VM~3b-BZpT26Z@yMQnNV z^Syn~9P$78e7#6Q`rZxRXH~;D!N^zf(X8~=>0>$Y&KsR11kj#JIeGf@)qBWzZv}kp zX;_;BjKjp5H3m&#DV^ z>n*T;sB(a;Y&%PR+-^2n(4gS_EpgBKuA8{dayv!{ZJ}~5XEE7#pg|!qus#bO4_q39 z3h~g=aKE&JT@{rzfo{EbYWuWd-~>Fd!u8koB5J>P3rz&#ib<1F7O`-xz3doXL(m%z z(DVO+4PT(U^q`aDV^JHF^+x%ytaJVu9vhMA;4@G~e-eV;%?x#@EelrpCQ5}*&g(RT z7L6b;(}is9>`Y+!>|_DgDQFfdxXSn`)1&0TFI8_`C1XQly%h`~v0ppqN1uE+S}Sq5 z2IgOMz~jeRDR{lRDWCHcu4b1H9b*TtZB#sKYHA5;YQd#2QHP8ecg|j7^oNT^v5ALK ztkA6tci>>L7wRYYu_xVqu5w;zH4~qye1%;M%bp1M&l)@i$1R^2OjX-57m9^?Nfwj?4rK${J>!`VIIYx(pb zc|LMJ7p;2RQJ@=lS_*RSgT4=P+Ld>a!#RDU9WlTiA)x}ae`|ngJ&Np?`GykALT13lbKxGokWjGfRyj3Q#6fh(_YbPVtZMn0nw6E^JwU zOzb-q*<309vvd17WRn@_B_kbD9q`U>IQnx7)c+(Xz?dTD>#}~RSBa`M{mcn)B_gYb zOuS7DBA=Kjfc#daDY`a}5PJZ9bag4W8bB@3qd4$iH=PFf@nVl@cdi=oQEflYZVC|u zNJe~Uo%8!y-=VD6irvljw#K({LXL6zjRHZFa6UQn#U};f2eA(-9fpxS?M|IS#mlS@ zNI)br>M!JTDKogu2rDeN@k7BVaqPmOOlhhBcDq;s#}V`aSVuQ!fnY1@Xt6vdyZar_ zL~k_0fOKmMxZDnQ%;BIgjNzdhJs#TQ`*SK36%LY+^pLO?8hNFArrxAsK52CoIlJ?3 z`~LH(Q}6$~x0w&GftJE6nD2a4fkcilDTyVxtt!wMm>0vvYz)ybWVacyetJ$3&P+G}eTkKMM+Op0Ua@&&56tf;sF(Ux;sn_G zR^5R}pd2*Zz}&IQ3>ZDV!(U#^_8gO(S&0b5Q&_|Q{`6X-Bng17IteVlz7FlaKXMEt z#&i6t02cHmjcD&mII#SA{ha@!%uAp4&Ud+IAc6(|f>I;S%itu9m$+s5Kf=8%j6Nt1 zNEsVL`a^J4&B4_43gFvnhJracd|Jw6~8%HTph zb`N~M0@|Icj7r}IT)xIe_`&S=4fQX*(45!hRmAQQ0X6d%df=0Os8&jB_NTbE8ea@T zgThS;2GEmE9Q?oRPaHq((2%(Fi7*0_r((mJ4rQO}17MXL81_OPYO{+Lo8Z%pc<31> zs1O&~X_lMl#%PBYRk0(dO^%;WK6GV5wan@M1s>6e&ai~{L_*M#jM}xt>&j-3zl||7 z`lEf$%P&UXiNC$>;NzVBI5jGtOYMAcx~wXV6SJM2QF$yV@j5G&Bsw5g_n=+gBM#_V zQqxUB9MZpVo1>@7&`^EycXpdGchnIiV&|05MdF=3sMzpPt~TO!4lQ3X7GdKH1icq; z-}L`KNjWmc5}fc;U76UHh0F%$L=8!}$_2E;T3;UB5cn%d^k6R35@#Xdoo@^v6ueGG z!x&n^^1aYj%k1-GPJ&9G8>a5<1VsF<))4Cx#`w(o$v@$b7*P2b??M@0{d^}Piz2#B zV|-(@rk0>Nf{bZ33iZZUsdL#>pyzy7`uYWjaINxJela~hw!ie~(1hT}ANzc0N!%${ zUUlM3p2afM$7qE@^cb}xJFM-QLD^YRQtG^e+&$Z)zgsqUZ>p^Eu{Bviu7QR%e}QQS zN&gC)_3(JA=hz~SF~YGi{6>}ib?+<)<*z&}!8S}MyRylEQfR2suE zXyeU)X!W8S8S`IW`Hv6v^&p1_zX-knUbSuArkR&Uct+lrWhG&Wu!GDIgEee*h$(b0 zRQ8KBpHZVr3U|(mHjWc@9tJo-0g_`BgF^&$z&upx`?S~lIw~zAo^Vaaq#?H}Jc&8y zxx$#*`!LG4bYW|>pUAk3=oh~b+yWC8`V1jP&*liKxc?3Uw?k2~V~#RjEoO-ient;2 zGu_MlKnRYK&Nrk<0jAfRR|?!kl&5}tanA3OadkDXkGcfLV7i5{ky^yyybnumBRV=< z@rBBEX8Z6{4{zNCH+ecZ!3URLUb_|Va!yX4{so5QR+m)V>+$Rsp2!wME4Bo)?Gh!v z8_|=xt-mgAD(jbqdu>xJx4f+X9ZDR5$T$rzg(AMBf8crwj({IOwIUlopzG^WSW~k4 zU+jH#RF&PCA|sh4NAkN8#X1e2_*!Sj!lDvl!WA_yJ6D^NUAi_E!`j`-QD-$ zRnIx^`{0(jr`*9=+E%|cxUWvB()sKp5?bqWY&D(TMCK@jZN;F&Kd1cC=ZnMZ3ysL0 zlhPL-)#K5F0esk-OS*Fm1J^%x(5)#`JG>_ zb@peP7R(p$*{}Euy#9@AD&o=#4Fj-o{Lmkg;!>ycXwgpcy?QrYSX^`VVq55Pp-v%> zJYuRdx?q7PtE=csx^o==Gc2NuMHLnSfS3ppZr!rC+I2Jye^8-= z5z-AiV4b6TDJ|6e-+RCN14-}}!3 zqA>s0iunHXy}wno`oC9%{=Zg4{=Zd(gCcT|2-KrpqYyzO^XvDpj5laxBunGLiAOkT zEL(Tj20sKR379lDyRzSOq!s$F(%At%_`?e*pJ82_0$`a4RV$9)?2$GUi~t()hs7Jl zq$6lL#=vC1Xbz;Fp^>>M;jc@d8fdXT(*D<9zhC{Gitu+S1HZNj04BJ372t-O>vt)# zztb=N{|f(~NcxM0X(RuGY$FcIReJ%R$^S`*U}|4Ssm?kNwxAiLf>zP>sjtarOi4t@D? z_NP~rkfUNyqr6~o;CbGE^Z@C@1s^%uI;Rj+>8m|}ZkXN;P5NZt^Rry{GoUM4bXOKW z^gP&hS#J8!vc1M(Em2eQaGl!#DEI;)X65lcjZ^pYsOMbEXpVDYubO~F2$$`b@c&>P z*?~<*;W!J!{AUo6^~w#ILx)+{?Z|mW_nt?e@n;-5mPQHcpH5Onzj2(}SK2)$UIs<~ zTzB7*iJUKfcRI{%WHp*ih8|eZ(x&{Jq#k^KFN68ZNhf0R z7zC0u=iaW{OGqrW+Eetn99@imfIxJb!8xru5*XW{SVCvzCE+Fi*~9#$u@(N=a~rr@ zMi6r~T`wEo=FCbYlnH^j-=kAr%F4kGwDIOA0?==Naf4?GOppXLeZ3RevaMbDcHfSr z)@I69B=J05E}=7`yV(dD-iHkJuy#}xGoNdxlWZ$}fG2U^AfO?q?dgl(+ zPFWSoV>M)*NaL0_{Tu3dsn)*l+nCAz414e5_{=GbI1HMd+%DUBge9-TVA3hUGOOHU zgrSnU%BiS2PAW_EpiPs->m@riG;)>~>xT+R4PATQDL9cKwV3q1bif#AC3<+*yYcV; zrNJe}!SE%`Ib+gLg@FQR!XiWpYtJiuk#x^;7+q;NPwC)`{-0rbM_z`E^@gLOHZtd- zP}9%i49BF@g?ei+_G7)kSLHepTq*b=D{(D%lC8&xou)>1^7A z68QKg9$HsQG|n#-^wi1Di#|GhBs$}@&&72Jr;G^1(Wk4%EBtVBN2^HJ$q3o1Sb?|R z*PwHoR=Va^4T+p!6!n}yz&2{OYJ2;=UKZli3@112!}2g1p%!5XoP~beq@pd2(YNjL z`mN#^cdeZ3PC#n;`n8Tq*q9!!AySykM+u_$-@5-=36cTFtFtWl{^W=(;Cq3#%fX=Y zgYR^V0OZhTan5fz@M*U%4-KiE=ws~Ad_r0YRA$7)spV?11AO2LNkZc7%Ka z@dYM(nePZ2EpS7b4~NG20YE-e8c#+VOA5F2gM2i5cSZoAitC)zl}@h2ec{W*w>tDo z+|5!Pa?C33c;Yz_ZRS!;0sKyJZfj)mQysURRYvH?+{t1C^x~uDf;EiY9(pCn*`O$f zA@%fTf= z9awr>UYyQjqVAX<;cfQ$Z2j|X1`y8wg~VSA;68Oc-fmt%>V_)|K6|@B7L=;siE5K^ z3Ib8|7o98?d;zykWTGV?-wM-{LubC&oD0D`Ct1YDO*VgOyWrEkcoTZiP-0SYhZZA0 zY=+-v;#ClwW`=MPpN7lY@^w)hno%)BJ7>mITp@4V=drhQ%e$w86uR;1aEcg@w^b&^ zputis6t29F5?w>r-%}ds)bnh3eX_14Nll=BVMDcu#u~HX=6&{Xhy8&yZHCB-QrEXp zLOReK!Mt)=%iHA~|6(*q^i}cC(aK~=QO({{fMU1_imGybQ^&dSq2@yAS8~JQ+_%LB zX6Qt#vU?wFQ!&g>63z~yDMeYU!;}t7Vvo2UWy2*fY+jD9v=&ar7QN~c)(T60$M$qt z%;-q_AArCZ9Xt+ooSB9xOc6r|u8~m(gMo|XHJL-NB=zpM%S-p7?#hhY1muXoU5%c< zy6Yb(9~3MjNE~lZMYTH?@P)cfAB#MI&l;SOWPqGk6d%Myw`N1$2Wr&xn5y1~c&@HeDJ{Z)9ex}XjT%#^eAF27=@UbrU z5^K|pG_6w=;ZLD+tzMYXAFdGSEutBC_U0L&H~bSjpg``pl={x=61u;G%ccX0qur~2 z5}F`*)rbtk^MAn?H>@UOfE?Pe)7!40Es%KBoifk?Ewu^1Ygh4V>k%oWwR;p$<}_&H zu3p-m(73Xn8#6Q_0|RFmY2gt%_!6`R0Cewt-Op-x63tlw3G)0$K_t(a#n+z!+Rl1H zfP@ntQ;D|&x4Y{<9QgD-_s_j|4d3}_f~Y~OqHq_!^d){4yYZDW<7;C(aSAkO ziYV&<9X5Ma-MittjJlfvFGoiy?jM7X+IDMaOf%Ke7h@tF*vwRg>Q3(@`^aT(Qo#iP zeHj5-!*bi7Ggh;0d?^S9X4;etq)ORZ`%L8fuIo}mcuRjq55h|TIAzBK^tZap`jwNJ?GRkqxl%KHv8saH7Ckx!VP z{#f;`0@Ub*rVG?o)=XJ>PgoXWHnq|AxW7XIKn0-iIztHbkVr$KZ$KF;GZK5=QN5c_sOn+GfZ9gwyzTB6}3#r=Kr% zUPpU1`2Mrf{`?B9=Ha*NT;F-k3cU=e*{B}fOwn51QHvkSkcrCWPhp6#a{c4iciiqe zML&m-tbwTR|76lwvK>mW!<{Ovw`>>EO5=|C3}1wQwSUHluyL$}r8{$--B)8cB|8AAPO!S-UX_Jxk2vNU~#h zIMdhyqzp=Qp7CfsZoD&4utG?yaEDz{_Z#j6Mtq0vgnend0kCp-RTzfYkj0*<)?gi@ zm&Sunl+@cD*PV=c`GqF(IP?N?ZJT`@Blv2CyJF&an(oA*H&)x>oCFByx?z|xi;0VrGyCwYwLP|Z=~^LJJiJd&L9RLV}h=Q<^Ry&_qdL?-!nw&hX+U} zIB}XJIL;+#N-h%_NN|%aff`@M3+UIW`+BL!s@xbyEo}X3cc&v*JM)+NEF1=?Kmn0@ ztfY7`z@kvCU^iXGAkS$Wd_1K_`4E+*Ug^MJwu8@`pKD(Xa!dOiCU&v@<%nRFap)NH z1b@J>ZZqS>E& zH)Oz$#ebMt@NDjDz=(t%2AQV-USb3S)hz$@RtA2?>wyF3Gj{9Cd9-w^L12C(&QBaG zCU!m6=0}k!)I-JfHHTSCac{?^KXaKK*vgkVO&F8i*h5oL2oSkIGD(8-PYx*+8OJ!- z+V-uH8SAWY!FO|P&PQ9J%i?r^1}EzY`)Wf+e&SNd#070*dl{>8GoLncZ7bGjDkZ>SVEhz6r;4xirW8nc7 z28Io29L#@AWt{tKQ2GC*a&OEJj1x43J!Z^6LBOi>w?Z3smv5ZGvy6{bjHhF!eNVxF zNxw2|CCvuJY-GpL*bm)-16(a_C&1USgUEc$VY5aW1iu5g2)t^NcO0}FE&(y!!pQe9 zelK!mg`=H+C&Yl7^KnFR_ZJ%s10XxCC5_42xE`RIg5Xh6@mPdCMR!nZFlmwvNM4p& z^t~}rYdKz?Qihb?je1hXg0oXFUh_#WSeoPM8^ZVkU_q`TA3WoHVYocQP2nC>ibL6K zVSxOY1e|L*@fi45uYT#zXzy#|ZDyw_**&xn_h5SWuBRjH91E-*b~jnP{D^8rYP1niUyC!=abmKlkQrRgDlFTUhE2q66h063IIVCTIBFwdwSPXv z8$Y=4&UrqbaPV%MjXQl8bLG}|rgL8eMU?Z7E{#c^{`?4;u!F+SkXMsOq%vd#_}T`> zyU3?C1>Baij;Y7uM`Pf+@OL^5J^WZNG~jsXxq^pT6r5*A9H0n({H&FOrcr(R%%NT3 zvwExGbBmttaD8_bYF_q`*Fi78)e*V~UM^9zKJAc?&^cLCB)rPfm|xYJy>O^&gL;nT z=9YW@Nm&Zy4E(Xvu?LpZ+fT;^U?H=)uyjvb5=(A>KP!BP^z9U4KYz;}{^^-+HSOEu zfG;$&MaCUp&0!56)jvD5)#-<~3$!29N)Z&Q3mf-$Mz4P+umD_d zyV@Iqu8ehc8$lA5_`025hU@cB>Nlu4B%8|D8v@3XO`0m*G%($Q_;2`pcXNBnLm=C= z-=cDPXPJ$TvHYaRkwPGzG#)FXEKg5LcdLFb57UNj;-a2&diT}m4e!swqYg`tEih}w z_zzpeSq=lf23<`E#N@w@8N$r0<1*rS7(u|Sj?{1MGiJ!%p7 zpG=D)a3B34Qshq}ppG!~f4_h8*3G{XoF@=QU@Ph7@<)t0{E#Sks~uT&M6!~gnu4kA zlB2VUlr+|kl(oG$6qQAO!PxS-g|3I3`h5@qa*Jo&I&cik?8Tqn@O}*J_@^Wk*=$RE zP>W>l+EXll#C?Ka_w}e7enKm`Zr#-U>kiPOC8<8a%37G~&dJFZs#A?W!uHn|1&}`5 zI%UEm5;=-4NC2dQ^`39y6D(wXKW1|MUi>zC=*iAPLUGN}%(2Hw0XEjWX$_Ip(kEc! z<>t~uilG8&0TZ~48y_py;yJ(w*q(e4Y?%M`6lf6YO&&{Cn2aq25+JBvtYvL1lWy`< z8<@O#(1~qEHaGwgqStv@jP=(S0Bd63&(!x;H6HGdKfn!9zupG*?VJ?%BdvB6x)uA}Sq zskn%-*>-2MufMifJ5s zXF_73LgG#1`-BS&zx#)Q@}%A$!2ejwTJfRc?W|P~%*y|vn@lgCd~Vv4jlVYTI5up~ ztv5z()){;nfTgUeOs@E=(>FZjECDaSZb)yggnng-9-9Ia1h+*mP8=qd1i;vt%=#5BxG%x#(EqSwFF6WT+%Ci8?CNd?Lo2%W>L;hrC^_2wN@i&}J;i^h~KV4p{a=C7(bg4i; zPo0(gWdyW`pH?$zeqbDzGwD9ic);%f!e-X}8gukwQ3~{VgNRR0&yV@?TW*QU%i_tT zESbR1BNkH-+l?WfFNJE=qRV#&#C*F8*yv%LL6jYDQDmPIB`GQnX3 z37O67IJlCwQKSU*W@~Xmx1mPN`1%m5N$odq`^B+k*fT=~jVisuYu8_#+YG&av;8b- zpQp6;GS;noHp}oQ-1jkmcL1PH+1`?t=@V18>2!4ysK>+;6R-%X)gHgBveW4!JAV39 zbGvvgFKTLVP;hW?S~)+JPb!Je@v`vvtnTWl4Nd68pnc^Y$BUv#m$?bSd^6^{_6M7*^MwCEw9bGXlT`%*Udw)7UJ`!Wp82cD)@yL_g1z14)P zP^PEdgc6(MuUEA_i#+uzN&yvT+HnFlHsqmmu*f#gMH09ZsS!oy5 zWapq4D>|ffUZC2`t0&R$i8Hk*gfd#fJPiF&V%qZivSpU6&c8?40}*25CVlBfy*aWc zeb<}U7N%2jJ(fQJPJDXcUVccr%FZo#ogPV+XVB}_cc9#+sJ-&7PL@RY-BNFC zJugO24vXOGOL89jS1u^0YSW(Mj+xQh)yP%rn&wV<-Nnc&YIaSC?)mHNmUlnK@+?=n z7OnafpV!J`ao8IW5{b=T&GebB`3avoU?oJxAlwfYjH@7siL$<9Wtmv-CF#v>TosqiqiL!7j~$<>1C@$p6kTN7PN8cy8gP`QYv3h z^af)iViX*cMpbZ-(vJP@^|1(LmY}U^kGM`}dE2He+irR;%fH9RbwPn(PJA2O_>&nd zkG!VC<|A$N+zW}X)|YiD29v$F7n#0Ar(djwArknRuj}hiF=>ly4{b(|)CPD0Qo%l7 zgI)+kmxep<4u2ku2#DpGl#dO&{j2u_)YxHkpV1=ggES#7^L7KJIF{JR0D4sq6|G9h zz|gvtds+m2V>T0igdMdIG3$M)oXdl+35Qr@yz%*uKwQ4UK@VQ`2Mi29?#anS-4u3$ ze~bF+g9qdYcJw`CsKet95UqSXUr;}(knO8W`F;%f{xsPX>_r@qp!)|Ja?uZ^ zvm3nrOcG?G-zw}6yT1mNKu5$3?AOZykGp+o9}XBAi=Okwwhfb#ywl zcE>_`kK4r~;N|+uDe9@NW~=#BlxTk+Dj@2$6edbVoXGg>#J=Kw>K%4{Y2$s>PUtic z$=wCb_R|}0SBI3s?dJ_1AWx5HPelLKJg~$Wr8x0!;QdY<@`zWY4XCn9nXEzO<;e;! zmzgetJ8=@KtXJ>Pg#$8&*P&JH(Du5~N==k^Vty0IZlScz_)N4Ergc8RI$%Rv(J-0( z)%9IVg8a4{r~i~SAU^{8?0z_%Z=iKKqmjRE98hbU?9JhewG^7Fv=cT~2R7&F0vuZ9 zQ+kvPx}XNWATCS3(=Mg{t<~kv;=5QB*XzqJK!&!gH|Lh*B33=x6o+J;S?=D@ z!toQs^y)FErpjq9KTZ<;mzLKdcTJCcV`sf6sp*(?r$v0Q)N*0hL7M>UL^2*z58PU$y_%ZZ#{W;AG(xwocVXGYz#zB(Ue7lPw-vE_mx1EmH zLfi;exzC(iS>YGyy_-}Ijjq~16^(Lj{NRm0dWgC1(qGC&XeFS8rq|y8kznZhOn6rM zdgzFDjo6jxee1NJ-UD+G$)K8%rmL>Ik;oXxH1+W0bb@K(h@W|SUWbl;p`)lPF4OHf z9>KH3wbE!^)+@NsGr^JMd^Sqs9^KZNF4~!_)gK)j&ba4G1bELY(SY0;>eZ((Elv;~ z?GkuKjnEQD;7tl#Vhc7C6mAS`B~<;CN5WNYD0<@}vkEmJ_b;Y}T~krDzq+^yBCPol zVNvq5(|0|FaKB66>wn#vL|Zj+-Xp}w@;*#bm0;9rO0(kaVA~~>AV0btYM>)W%LYqW z17b^ib!#%d5oP6N{4*Z*Mre9QN;0}GObRooV~`@1bv!#s$q6+ZB4p^3NyYQ&XokHP z7RMAea3TY~VdPBv+AUe|V`a*jK!vZy%kpKUe3!@1hU>{J_X?P}Fn9-gFTDupU9c1m@vi&em)2%N1oJnx`g7)bG6A`{Yd;gkr zA{4cmp+Xmv#h)El3&rQ5YVcHg?+O(rz#D1l-<>`NbB?&rLG#O2yRy~OY24Ljewgoz zIs_upM6wEM#9Vc|Q^XcN8omH$OwPJ3`0e8Asq{2Tqz8ot?n{w1BQ=9ARhWmbamJ^J zo(~)F3YY~M6{lsAy1pckfnvv-${l zW`msSIp9+N`#F&t`PuwlkFWCOJ?w$BeHq>lcdpg$^|Js+Q6^-5px7|Rf5X6Wqm&f= zKF4`|C!;VI%|5C37V1E7@5Y-0A7Vfk|BOrtTM-f{OOZ0pa&dQmVK|}qGC;*^KQn|! zcDdn8mm&2dvT!j2DcVzU{cn-EHE9^lt-G~mm<()5e5r#4VkBxVqfbzDbFi6d5jm|f zQp#@7yehbT=4r6yb7WvVCE^|r8OxnL%DXZfn&|r_>nl&KACRpQbU^Wq8I&|)b3agI zM;?ChI&ecn_VBNvX2^Jsg?A34?T3YCL@;m=W+ljcxGiXLMlTjHZ`C3bR}#fp5trd& z%Qqk`7KldcA2oQ2kG=2-_cYxlLc-7r?_q@wfqJ2{cyhlaY4=9BXq(KYdC6mDg+tm~ zdhy>eO)7IdN}6X{%n_oN}6V}4!B z2E9d=pS{{7Aiz;QdvWvDOLnM*vcvFU(K2&wtiepxNQr^lY|K{9U9tLpGIeFhm; zm+gggb8fic_<;NVk@l*PngKHIWMgl5eVXN;M}uy{zcEKu&_cA;q-n6oM%E6vX(a~V zWOiejlHQk8;gBQjr&C5NZHP)rdmIT}0ur*~RN(roj;XZ`)5s2Hoe0pN$@v=GiufMc z>lS_Ct1iXIK_{aW+XZc)o}JBKWPYMVyw5v%&mH}~`T6$_ zL0T_qxrm4DZzUe&k-aKW1r7+!6Wmj4XCsBRr9E-G8>~wCkOZQ0q_|v^)2&Wa9ixo& zerqeu5c~La)t-uZJh$^%)@*O2(#!ay0r?Y+L#$K*WJvqnmNJI7;Xfmu-@50@mqRZ~ znW{DmTgsD2Q^|A$@DQBkWK`r{AnZ}^rm}KWWn#073>2wK=TahY-@KzyWMz+v5)o?b zoyai2ytQbuf{_u)Hbku5+}KS{Jb;kNrmRn_VM_{Dt#ciQ%KChZ%gan!^G;;~nZA3e z!|ER98mYrn$mL=@otI?n?|shzl4+DLtk*r%s}0T~p%4?72&_62rg9x}jQc5b%1kns z*!g-P($EU$LT{~RZbzU~BYRs72GN@xVS*2Jy9(ytLYIYf&iE}PbzAiSAv}&(^B(6E z9!)5RtNiM|`0?xx1l+dOx2OdvF-e7ltmce8FZXl#fF+k-%|#jAuBl|6bdpaRrK+D&3d;Ht{S z@*J{oD)?vH2NXv#fh;Vikp4XVm-FRctT{svnc@9%_cU$+$8?_EA&>q9R%W|{8;#kL zMUrci{3ZytkTbfWDYeZ2m$`_FKrr?y``&{w)`E$j6gY$gP+f}ocIv!jy;n}Oiy%Uc zC-ovVb7^}x4%TOoY%%BxskKP~@4uEAk?et%Lulf}Ch{<z2qpY?irZOged>mr zO-pv{^;})hPSK;~Vd=Pxwt){i9{abq&iWPIFEb61Wc6YJA3M-iJyrc#Y}Wbsm_juIp*f+@#%XcucHVYF|EKK+ z7+?xN_`BH;7|uA$v-}B|cI=EGXrtAOkK??Vo{rK{LQOFQ?AIDCcEAi;cE!Ik$Pn4vlMlp0cVWi+Vn zn!FOV#4BSAaHJ{QFT&uiH!RMEhOH9FKMYVG>Kv9QuuT$QIq81Hi;iss+uD&a&Nk(sKzd6~{2 z=)RW}{7dT|wf~696w5BZPfc73YyDx|1w9e5srKKKpNmBcZ=HXwhu7kOM~y2(@x0_< z?b_c*LY9`ewBIYM0MAffQ*nl%vCnh{$}bgEGGC(>$EPEpLrao+qf~i36k-6S3O<7} z)b-DLET99fl3XW35B95-wmy4rEBl2JG67J$xW|P`{~gUzxI@)h=Q@R4+gI16s{4IO z)zSe!is!J-A1c1&=7cNywkcL-0bx%h$^n@fGJ`*bi{Q=T5kX99>SgW^Xy->Zt7~UB zB7uxlu1s1GGhw$7bghe3GZIxw&P{#^Yr2^r`V9fu^|~?HuLs#VD1TSgH*%XcNj6tv ziN-acBZcQ1jir>&ZL4H+*@f@C>kFOGl_zH$T%MvjtV;B`8y9NLMxZ&9?k#a=L4i4bcn4M;6MD?z{6TF;eAc0Vcw z73-rBBVWm3fm=U~|A)uOT33hLy07B%F(@J^L53`}@=U4$>K=o!@~dkReIrt#k51BD zEBGD(U>eE3R(v*V0%Dmf;?@lMxg1p(-toNb>{gywZOh+jid7(;L>KUJa3`m~{OZ7% zOxb*}vS$fTob-F&GYO}NXk0YWJncjG=Ephv2Gl?hh+~G>?2-k}Vzk^f4W3d@T_xvM zAt0sdY^Lr|zyJ;sdc{t^uT=)eVx3r3AE&YMV{FqOF&=eS$yn$)Vw$}Q$Pe8IN9W$kb4h4jYWtlPAQ#nEdZ?cl zh}%kK&;Iq_mcTdG86Ip36H`_n+m{i$Cf$A9{|Ddh7m_=Eb=NV$MiQsIRp0jw+M$!-aXDwGEtUA@Jx_C%6EwB7_!QZb*2phIO-h$+1AdK22KD_9CUQVvvk>LQ?QW^kPb zY7Jj-1Ant8GBDUqjrXiYIj@ZwdxtaF142!tzqC62}^3Y&xJoP_(bDEC;6sr6;M(lwP zXY%NYUHTZ5u@aGJP$R>3$~EW5z+rD*z}yr)uhAX$#)Zs z+mY_%yl+XkI6m+pgP6k+i=c2fWg8P8<Rk z)#zLI&ym5BI?}sxN#v&#DMpBHxjQAEGZxx9tI7L&;It`3$`J z9zhN7pI+Co#nMJqOW06^&C^-1=~FPrM-l_8_s$%@6R4-PUDbksX|KPW{qojjprdx} z+sJ1|Qv)_fVzRz3z9F<6F)1vd^Wnvj2oeduc~rcfT)7`sXR}vVSQSETf_oqF?=z$) zQT$DO5b2Mv0D@D@TN?0f2&rbdr@--oNmH!h&axj@?P6X;0j|AWuhj_5-Y!vU{{hb* zsM4&efIW_|*CTuPa_SkGoMQY}aP%|vs(d3-!c~&BvjRaW<&MeiW0u{>{4S`i6rFsk zF6>bHkeHx60C!S`?!ChL?%fHNDSsQ4wQxYMD<{h$cxK+FyyjTCtDcm!!tN^&GfV2ejYo_kAp0$5M~3{!|N`Scxwc6DHWXXI;7EjLMKl90?;I( z_O)}qOnJ%)*Dxk7lu^LCMjH}4%$1{?sgh+rZPt{XkL0)*+k(^$Rpkx{j^AWiia>&QOH&w^-hr>;=9C@i4Az{U7V9Xd z8&YSP0iIQ5Mo!z0hc$W*c=kyTMz^x;+!{bZXT8mN`^xDb(H?pN8fHKfr!?1kOwk}V zp>05*zK(s`l$>!Uv?|1JR-jVM8np_g_!XcvT^)VV7~=$=j#>=AUQ_P$qPYj%KQdY8 z+f^=3{ znqfb_>Yy`2xZybxmJ}~^f%(cVbTeN; zG^>7-YBRK2cV@-jsdniU6Aj5XfS zf1bey?C)@e-K-RAk&u!`XWn}*?V9+Z0_(8|o}Fwo8C=rWgVUAC=MI4#t31Z1`BuPU z--ufKEO%SU3nhJ`c}fHjz0a99Cmi7;7lzHu17Dq_=-rF~FDM=($$bK}xeyFb8m{y} zc%}$pf`jQE_G=kYm-!M-sVf)81_yEfaK$IXdEdGG4)DxAd{PsfQA%$KfBfS)edxn2}GBpro_ zDW1qp;{i6rPzmIce28zf6U>!x^@9MpRbw^a_7JHRTaV|oy4_gbfOsv&-kVqlr+SCf zjJYuloVAA-K&$6wS82O!d(VYqJo%RFRODR;JR}&GBi{f(p3;Ed%q+{o6>ugEscsaI zGlL9Wn}KCr;e%Q-CJ79;sLzr=)^`>)9R(Qs`us~?ssP4SgjDY7HEi~cMB;*@9~HT2 zTIuoJ9H+RUG#Bxp1r3wJ(FD2#5X`5?TheG}Kz#@zZb2iJb3rc(=2m{yI zyr^PoB}w^gG}pN|;7=*!AbL&D(^IJ83{36xzw8|W{g0piZ~b2>mV+oCQMIgLlbS)` zliJ!_0bhLpA!@IE2M)GxyNxX5@jUT`xk)~_IZsQ{#$OVq!9i1o?!pc7yJC48(jGUvXIG{-(Gx!hB13{Y7;w(yPJeO6Dm<_iCr^0ZfSwm)|5er!F4bW_Gyv|I~fcdiCKDn>zPb%#2zS%D6 zm|B_D*`HhdP86^{zfbqxH7fOL9<39tugvW71+KQ_xO68*YFZf*oBqv=a;K7+0Zq-$ zo|f;ChKgBzAa6RWsTcQr2W_~)Lx;D7hh0!vT_P#+pb)7gGasD6y*qiY-*-2*jlN-k zi3XDZNr;ZrOL6p-nL((i?)lQfoI1=Pwim<{B_IRj29uYFUIMP>-qR7wY+F}fTO!UG zSl`Dn1+NO}mI5-MkuE)MxKo@g4@VHpEPKfl_%c%m;eE+(>L5pW=F3a8^6F*z# ze8R!QVVJ?&O;Z=T*6V#@X6jmEqT=IF_hN!EaJHF&a^M~>_lCcLBvTek(Q%)~7j}@y ztCti0T8}PN$5ytctG_{cF@F{N2AQcpT33RSY9$JRA)S>yX<5{~;t#x3GL+(%HUOw~p>>rIOqPC5~FFbAjU~#UuJu2abOd{V0!Y)b)taR~Cb5>B7 zJF12~Kfyw7|Me-*PzdWGPCGxkw_%+uxY`rV79~&OSwOw&6@BalcZcA z8jIrouYTR&5R6IxIo~?A!UEiAxCEjc9rciHzbX-d9~_~eG(+F)J^FVHmlYO@2plg* z8~bzD@JJKqUxfkjRWdKud*t+xHVG|Ilm}RpQ7Cxg-ZS(2Fg@_V>LyMIcrX&$6*-gpF=xqaef(_lSM4`#@<-^{*TLe-mpl4d z3uiL;enr8E02|%AC9s4gSkj$0_PhX#VuNU9A`CS&Vkoddm(woQ1qrX@Yf5LW2wH0Rp1~vSg1v-}@RHerQ0v=b*Tcyp zk%Rf`i}oW90t|W!k=%%s#ioVt5Tffz3i)^5<(r>U%Qq>KH#S1A$H}fLI8GL}XNv2$ z`Sqe`WVD1JouwOXp@^!E{=L1*1YKV}oNc~|_CG)f5eH+fPzxLQP$eIPZ$*BF>lgY! zLf>+MJ~C+JB?Zv$o2#LVW&eU&;N8*2YG5@!_x0C0M~Cgby}kJvy%HW_Vc}}-)V{9L z?)@4;SXb#YnFtG^S3vEu5sTXT$QHelxj(oB!H5h$ePp(&kP#ecf&qMp6H(F+`b8Wf zkeZs|&24fcxT;!oNk0G(adQPD0yF-?GbkVin5Ja--(_iTQ-HEe*Zg4-G!}NI33VQD zduQjt)1x;Vhp0`15tnk{P4%&p0IAU=MIi?8&JqQ)3OhkDyevf=Y_TVa~ zj}o4w_NWbz+YSo)gTBx(20m*q>9_s*3IvKa97iBZ#|K1_G%^#trQ@YHx4uFkn#{4G zHh<<&Nq-O_OWoEOcndMo3Y8zU*GCHUEsU-36p(VtMr3Yo@D$9Cf&2C?fqwTako~6Z zYdD_U=@{6B2V&-+k97J9BL(?lmp|+Y9!sqVny(|^5X5{1bb2iJop{ffNzku> zAOf}2_D_uEnkEdFz%breg#lGGWm?aF5tNC@e4`Bf_0F5d=Mx{qEWyYEZtvh=2tQc) z^J%Tq?)-yRXzE3TCNRtzAFz-7a^86xt8M&h$NsbM>GDd)>xg%X7G;q%GTt&*ef^Qs zR|XzVEQIkoDwPI&1KjhuvyRZQfUDeJNQ#+KN^@}$8SQ;9dNo?mcIN7rKK2U}{$`jb zkbGBV^8_Qlof#&UVC58rotv zJ}2cme5cQnl$l`b1RocEWN3`b0g|7ciOWjV$Y7zua&F|^S_={ae| z-p&fUT|aMVZ@$8L(ns41JyaC>f}|tf0$FX?I(r!@kz`96jP-%CscJme#P%PIqSPVx zx1BR9=H>0(%li<>{e*1OV}w@}DH^~o|EIDmkB6%J|5uMErM*Xmk@_Vy5z01MqDQ%7 z2~B9R7R8KhLK3p{lq|V~A*L+3ghZ*aH_?+NOPI-;tumO#t|3dK@12LM@AG@Tet-PV zAM?lDdp@7f`FuX-ocHIP^Evm`L34aYjxpz+!O#P&OvZgd;N7>>X=|>&9KpHSQ53pw zv@iAnTa^la9wAW&wL2gDP9j0s=FDo)$DTrS z2LF_Sm`@4k#eer9p$FkFgcL?Wg(J|EiIcvX8^telOark@x2`LhVip4v%n7?)2RLE? zusZO#2A z3N9sFEDja`CT`!c%-VZmn=7HE^s6Uk03yT~z#$Fxn_6a+QNZd#t3ukvMhv0AI4*w% z|3Qny&N3U5Y=yk-1%Z)@>$rWx2QaWl9r5FS6|nX`Q7dIAgP`Wj?f#{lY}`jhRfB*_ zQ||Dk%jA|5FLf+;4!GUVm>_1xf{83F;tD7c=7N1ZFju`?#r&!@jmb*Mrss1ig@ba;{TU#eYmA`|x}} z6GfI4Zmzo!9OMqqD;4;XDKRkeb8?~Em3_?<2UNtC^>p{#c^q69NjU8QPBXKnj=BE) z)o5gF8Bgl%%~EuyP+A#Jn^ft9D`j{fsO-Iw(n~F)(>~NQmq2<3R0u-Lh z%j9;7`k&8eh=?IGzH9L^qL;leX#aD67w%bK*tUMEm+hB5e_HN*CeC0ihuV`M0BC#| zsrENlvo*`dn%ibd`;fS@e3Z*C6Xa?zt~ZP8)%o}j{3i2hmflUl3T>A5%otE~UMl== z_9D(w#n`3cLi!od-{3Y+y<%_R`k|-&ALt=p!?)_6RW7>P%2a@^2J8W-G%p7sF7+?s`$=TVUi=U~rWfn( zG_(PjRWTlYb=FWE){haB&LD{Hdxl<$wp&$L--VJD2I+iHS(0Zz{-YFdYDe&Q>6mKb zl-ePxRr!;Hzx2q7wa~MJAxEc23o>Hr!md%RSM~f>1*K(;k}S502Fo8*fD8_x(?X<{ z8zTFFEWRXLomw9&1$;_)kw@I;k!JP6!OT`X44Od$B}x=Vy3X34ED}Eh%D(yy2ywS@ zBayE9?`euiQ}(U;HLfiv73fQ1q{a8)!!`jIyc@%=B{HUevVJ+;ZxF_OhOepsEw!zZ z0znl-KL?7QB3!!}^#xI!Qx_(p-^E;cCZnh(Gbqj z3lIdDX{StgAoerh$5Z@rh1E;UIwjfJiy4?=>4ggdLbYLA_03wIE6YT!|I>x0u|&rX zo}m-6#vz>|tAMiAzzB1o#I3|RX|Iy-@y(dgrFW1Jw8j^1F2osF0YSeFZ#6Q6r@B|< zl(^K5rG-N}T+k?n?<38wp1JWVE15|V7+1s}cs=1Pz3YO&Rk;=uu{bA$_Og@}?(l>h zopCaUrROIIaMDiL>{T26V-1k58>V^4$s94ul5~gM zT3+LX{gdWddiRi4t`GC@u~0s%^MOf}z@xY-L=-2i-Ggzp)m{Z`yuff`G~EaLFY#{( z{HGv|d*<~!Fq>|hb9?5Ojo@mqOQCfuPv(gfklL4ed$67K2TIWw4&=S*w>pS{OSO>&1FL}Y61Lxr*|{>c_>n2jaT39jz^7QIj{gObMbfzZjh^Bcz{a0D>-jgz zn>nCmef-;pB5!?qW=hOwI$z?&N27;XMis{Gg|AKUaYMb6zpeYYPrE03KeAkaIc&!Y zjfTUQ^YD&zW7p@V&BRI}?S4*!6%ScRPd6XWXV^z+=g)2LqBTTnjQYL_-VwEP!6o;M z?436c$gfM<)G;^MQYjH(JKUGoGmdn#{YM}sGY&qw3W(1WJD!sx=_{*RI#Y_#n*D5I zcJg7j>C^cANSammH~CPXi8moRMZPrU`O)#85{U$3E6%cO&#%)G?b_Rc>1!5J{RBh*d&FZ zoT%(mXYJj$FjT(y^2T`IV*VMc>Vp=vVmsu{rm?bdaN;WRu`H`AZ>sF8HBEC`!@pB; zWShBazwt6{d7;&--O6~8u_kJAzS+eQa6(;Ar{`ehK#a8Em%^7#YU9a%;}6O&{3&Dz zpF4m=#EhNG>2yK8dR{F)K*g#3Eys7&fDAUP&8xldoP71snw^1mc|$JGKR^C9*rYJ7 zHkoefN%yIa^e?o_nN4|@@a14eJmyP!#%$%cE8Mzu%e5Mm0wcx8hHHu%Q=Y4-&bN679I|qu;IRb_lx$Ot)8Yk{pf9q5g_P9( zhh)l~n29w2d%gnOYT8!=P9A#EEw1Kt^{M91AL~k9Og^@(+;Rf0iRf|c`_1EUvj6s? z_Yc)%?NS4OcqVzc=N7a_)hFD%c{4osdOwZyp`Z0=E3?v!emw&H=wNKsropM)W{Doh% zOp&n$ui-|=`2gPLp>1))Th>=?O3WuXn8+EKc)DIqun(ydiXD)+-u&n-FJ$o5+&6N0f8#49_4I3Y@Vh&8({DFwdpxgA z)4lu6@??q@EUhYzbGxx6eg}s4V@J6hbtHdCOH65-tejSS8#7Br)oB!Jvv9n%gaT4M zxgaSkW)0F>-@aCj%q^v=s!tMl8;@WE=F@7WqO`8DFX9U|pFHIu>CcAAnr4S5j}^st zu=~jL0v8tw<1~Xsb$(_GbzlQJ4?<^BRy<^Y5BVX^%Mmjx2a7s@Og}xMLr!iM! z+$?&L3mvx^E&gZ^L?#H0F`6@C*utKGOb;GcsZqYyxi&`Hr#fbN;!AC}hs{i^ZFdR) zE$!(b`(+1PNc+cmpP`E(yAtmuoGgP}q6L7mcM<$DUui-Vv51zHKgFhH+ zE49O?Eu2TEg@lB1ioR?ixhGmK?7A}X)$w7$*FnpHI$w;-$fc#e7PNR5(mCq&Co&D5 z3M^OFDX#|YYqTtT6Jv!o%3&}@NvgR6h1VB^i;xc+7Lfgigz97(x#go?W?RBZjyU0I%JoR@`tnpe&_$*>;3al$jc(!70v9>Bz;_Rx zy&f!$>!hkU&lGlzQ!M8`6;25_)mMWB7Ouq|Y?i69EVcVay{on4axY{|KPSD#v_sUHN2n_fWE5z=aE`=hnJ+ala~Um$P_fV2O-3`6_PRvqy%pQC6H)%P8>} zxn;CAdgEFk%R2|;^ml{`m(TU2*e$L22L-h2#Yp*Y1+XFN$zmunK`w+uoIt!4ouApz zUc|>BVH9_AK?GO?flABD0F>2DknxbX0u}ywKd1o>zyPSx6mbS|B-wnaKlt*e*;IKn zUOHwkO^N~lFEp?L#AWqw3IgnKlz%hZ-3vHojfV0-Z8XwPPw4JMQ8_@5-nSMoJAul_ zp9({lr_y5{g2)cQQYQ!aL%1#heRNMk063L_-GDBl1tAed&>mC?sRBTSV)%BHrrrhs zkWrM%=U;}*FSW=W!UQ7}h{YiKNTZ@Kny)S0dF^VjKl62)(7%taiP|?5A9yCt|C7t6 z#UVQXUnhYgq9ypB2mxKp%qB!tmY7Yrs37$JK=qM|po(^u-HGLYS#(iI~iyEbww7C3c4Ajo0m5kK{PNf6{0WyFvBmv>44 zWP@V~LL$#mu>fRCXc?ydAN5zv!mls4lxdF;_^-cF!T(>pCTy$8*@cej;ud_F^>vL; Jp#*> and <>. +To add runtime fields to your data views, open the data view you want to change, +then define the field values by emitting a single value using +the {ref}/modules-scripting-painless.html[Painless scripting language]. +You can also add runtime fields in <> and <>. -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Select the index pattern you want to add the runtime field to, then click *Add field*. +. Select the data view that you want to add the runtime field to, then click *Add field*. . Enter the field *Name*, then select the *Type*. -. Select *Set custom label*, then enter the label you want to display where the index pattern is used, such as *Discover*. +. Select *Set custom label*, then enter the label you want to display where the data view is used, +such as *Discover*. -. Select *Set value*, then define the script. The script must match the *Type*, or the index pattern fails anywhere it is used. +. Select *Set value*, then define the script. The script must match the *Type*, or the data view fails anywhere it is used. . To help you define the script, use the *Preview*: @@ -46,7 +53,8 @@ To add runtime fields to your index patterns, open the index pattern you want to * To filter the fields list, enter the keyword in *Filter fields*. -* To pin frequently used fields to the top of the list, hover over the field, then click image:images/stackManagement-indexPatterns-pinRuntimeField-7.15.png[Icon to pin field to the top of the list]. +* To pin frequently used fields to the top of the list, hover over the field, +then click image:images/stackManagement-indexPatterns-pinRuntimeField-7.15.png[Icon to pin field to the top of the list]. . Click *Create field*. @@ -54,7 +62,7 @@ To add runtime fields to your index patterns, open the index pattern you want to [[runtime-field-examples]] ==== Runtime field examples -Try the runtime field examples on your own using the <> data index pattern. +Try the runtime field examples on your own using the <> data. [float] [[simple-hello-world-example]] @@ -110,7 +118,7 @@ if (source != null) { emit(source); return; } -else { +else { emit("None"); } ---- @@ -123,7 +131,7 @@ def source = doc['machine.os.keyword'].value; if (source != "") { emit(source); } -else { +else { emit("None"); } ---- @@ -132,15 +140,15 @@ else { [[manage-runtime-fields]] ==== Manage runtime fields -Edit the settings for runtime fields, or remove runtime fields from index patterns. +Edit the settings for runtime fields, or remove runtime fields from data views. -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Select the index pattern that contains the runtime field you want to manage, then open the runtime field edit options or delete the runtime field. +. Select the data view that contains the runtime field you want to manage, then open the runtime field edit options or delete the runtime field. [float] [[scripted-fields]] -=== Add scripted fields to index patterns +=== Add scripted fields to data views deprecated::[7.13,Use {ref}/runtime.html[runtime fields] instead of scripted fields. Runtime fields support Painless scripts and provide greater flexibility.] @@ -168,11 +176,11 @@ https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless [[create-scripted-field]] ==== Create scripted fields -Create and add scripted fields to your index patterns. +Create and add scripted fields to your data views. -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Select the index pattern you want to add a scripted field to. +. Select the data view you want to add a scripted field to. . Select the *Scripted fields* tab, then click *Add scripted field*. @@ -186,9 +194,9 @@ For more information about scripted fields in {es}, refer to {ref}/modules-scrip [[update-scripted-field]] ==== Manage scripted fields -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Select the index pattern that contains the scripted field you want to manage. +. Select the data view that contains the scripted field you want to manage. . Select the *Scripted fields* tab, then open the scripted field edit options or delete the scripted field. @@ -202,9 +210,9 @@ exceptions when you view the dynamically generated data. {kib} uses the same field types as {es}, however, some {es} field types are unsupported in {kib}. To customize how {kib} displays data fields, use the formatting options. -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Click the index pattern that contains the field you want to change. +. Click the data view that contains the field you want to change. . Find the field, then open the edit options (image:management/index-patterns/images/edit_icon.png[Data field edit icon]). @@ -261,4 +269,4 @@ include::field-formatters/string-formatter.asciidoc[] include::field-formatters/duration-formatter.asciidoc[] -include::field-formatters/color-formatter.asciidoc[] \ No newline at end of file +include::field-formatters/color-formatter.asciidoc[] diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 5b39c6ad1c4cd..b9859575051af 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -2,10 +2,10 @@ == Saved Objects The *Saved Objects* UI helps you keep track of and manage your saved objects. These objects -store data for later use, including dashboards, visualizations, maps, index patterns, +store data for later use, including dashboards, visualizations, maps, data views, Canvas workpads, and more. -To get started, open the main menu, then click *Stack Management > Saved Objects*. +To get started, open the main menu, then click *Stack Management > Saved Objects*. [role="screenshot"] image::images/management-saved-objects.png[Saved Objects] @@ -85,7 +85,7 @@ You have two options for exporting saved objects. * Click *Export x objects*, and export objects by type. This action creates an NDJSON with all your saved objects. By default, the NDJSON includes child objects that are related to the saved -objects. Exported dashboards include their associated index patterns. +objects. Exported dashboards include their associated data views. NOTE: The <> configuration setting limits the number of saved objects which may be exported. @@ -120,7 +120,7 @@ If you access an object whose index has been deleted, you can: * Recreate the index so you can continue using the object. * Delete the object and recreate it using a different index. * Change the index name in the object's `reference` array to point to an existing -index pattern. This is useful if the index you were working with has been renamed. +data view. This is useful if the index you were working with has been renamed. WARNING: Validation is not performed for object properties. Submitting an invalid change will render the object unusable. A more failsafe approach is to use diff --git a/docs/management/numeral.asciidoc b/docs/management/numeral.asciidoc index 893873eb1075a..d6c8fbc9011fc 100644 --- a/docs/management/numeral.asciidoc +++ b/docs/management/numeral.asciidoc @@ -9,7 +9,7 @@ they are now maintained by {kib}. Numeral formatting patterns are used in multiple places in {kib}, including: * <> -* <> +* <> * <> * <> diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 51821a935d3f5..bdfd3f65b3c87 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -5,7 +5,7 @@ experimental::[] A rollup job is a periodic task that aggregates data from indices specified -by an index pattern, and then rolls it into a new index. Rollup indices are a good way to +by a data view, and then rolls it into a new index. Rollup indices are a good way to compactly store months or years of historical data for use in visualizations and reports. @@ -33,9 +33,9 @@ the process. You fill in the name, data flow, and how often you want to roll up the data. Then you define a date histogram aggregation for the rollup job and optionally define terms, histogram, and metrics aggregations. -When defining the index pattern, you must enter a name that is different than +When defining the data view, you must enter a name that is different than the output rollup index. Otherwise, the job -will attempt to capture the data in the rollup index. For example, if your index pattern is `metricbeat-*`, +will attempt to capture the data in the rollup index. For example, if your data view is `metricbeat-*`, you can name your rollup index `rollup-metricbeat`, but not `metricbeat-rollup`. [role="screenshot"] @@ -66,7 +66,7 @@ You can read more at {ref}/rollup-job-config.html[rollup job configuration]. This example creates a rollup job to capture log data from sample web logs. Before you start, <>. -In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` +In this example, you want data that is older than 7 days in the target data view `kibana_sample_data_logs` to roll up into the `rollup_logstash` index. You’ll bucket the rolled up data on an hourly basis, using 60m for the time bucket configuration. This allows for more granular queries, such as 2h and 12h. @@ -85,7 +85,7 @@ As you walk through the *Create rollup job* UI, enter the data: |Name |`logs_job` -|Index pattern +|Data view |`kibana_sample_data_logs` |Rollup index name @@ -139,27 +139,23 @@ rollup index, or you can remove or archive it using < Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Click *Create index pattern*, and select *Rollup index pattern* from the dropdown. -+ -[role="screenshot"] -image::images/management-rollup-index-pattern.png[][Create rollup index pattern] +. Click *Create data view*, and select *Rollup data view* from the dropdown. -. Enter *rollup_logstash,kibana_sample_logs* as your *Index Pattern* and `@timestamp` +. Enter *rollup_logstash,kibana_sample_logs* as your *Data View* and `@timestamp` as the *Time Filter field name*. + -The notation for a combination index pattern with both raw and rolled up data -is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_logstash` -matches the rolled up index pattern and `kibana_sample_data_logs` matches the index -pattern for raw data. +The notation for a combination data view with both raw and rolled up data +is `rollup_logstash,kibana_sample_data_logs`. In this data view, `rollup_logstash` +matches the rolled up data view and `kibana_sample_data_logs` matches the data view for raw data. . Open the main menu, click *Dashboard*, then *Create dashboard*. . Set the <> to *Last 90 days*. . On the dashboard, click *Create visualization*. - + . Choose `rollup_logstash,kibana_sample_data_logs` as your source to see both the raw and rolled up data. + diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 4010083d601b5..2b00ccd67dc96 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -363,3 +363,8 @@ This content has moved. Refer to <>. == Index patterns has been renamed to data views. This content has moved. Refer to <>. + +[role="exclude",id="managing-index-patterns"] +== Index patterns has been renamed to data views. + +This content has moved. Refer to <>. diff --git a/docs/user/graph/configuring-graph.asciidoc b/docs/user/graph/configuring-graph.asciidoc index 968e08db33d49..aa9e6e6db3ee6 100644 --- a/docs/user/graph/configuring-graph.asciidoc +++ b/docs/user/graph/configuring-graph.asciidoc @@ -8,7 +8,7 @@ By default, both the configuration and data are saved for the workspace: [horizontal] *configuration*:: -The selected index pattern, fields, colors, icons, +The selected data view, fields, colors, icons, and settings. *data*:: The visualized content (the vertices and connections displayed in diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 1f38d50e2d0bd..9d6392c39ba84 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -4,7 +4,7 @@ [partintro] -- *Stack Management* is home to UIs for managing all things Elastic Stack— -indices, clusters, licenses, UI settings, index patterns, spaces, and more. +indices, clusters, licenses, UI settings, data views, spaces, and more. Access to individual features is governed by {es} and {kib} privileges. @@ -128,12 +128,12 @@ Kerberos, PKI, OIDC, and SAML. [cols="50, 50"] |=== -a| <> -|Manage the data fields in the index patterns that retrieve your data from {es}. +a| <> +|Manage the fields in the data views that retrieve your data from {es}. | <> | Copy, edit, delete, import, and export your saved objects. -These include dashboards, visualizations, maps, index patterns, Canvas workpads, and more. +These include dashboards, visualizations, maps, data views, Canvas workpads, and more. | <> |Create, manage, and assign tags to your saved objects. @@ -183,7 +183,7 @@ include::{kib-repo-dir}/management/action-types.asciidoc[] include::{kib-repo-dir}/management/managing-licenses.asciidoc[] -include::{kib-repo-dir}/management/manage-index-patterns.asciidoc[] +include::{kib-repo-dir}/management/manage-data-views.asciidoc[] include::{kib-repo-dir}/management/numeral.asciidoc[] diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 64ba8bf044e4f..f6deaed7fa3b9 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -5,21 +5,21 @@ The {stack} {monitor-features} provide <> out-of-the box to notify you of potential issues in the {stack}. These rules are preconfigured based on the -best practices recommended by Elastic. However, you can tailor them to meet your +best practices recommended by Elastic. However, you can tailor them to meet your specific needs. [role="screenshot"] image::user/monitoring/images/monitoring-kibana-alerting-notification.png["{kib} alerting notifications in {stack-monitor-app}"] -When you open *{stack-monitor-app}* for the first time, you will be asked to acknowledge the creation of these default rules. They are initially configured to detect and notify on various +When you open *{stack-monitor-app}* for the first time, you will be asked to acknowledge the creation of these default rules. They are initially configured to detect and notify on various conditions across your monitored clusters. You can view notifications for: *Cluster health*, *Resource utilization*, and *Errors and exceptions* for {es} in real time. -NOTE: The default {watcher} based "cluster alerts" for {stack-monitor-app} have -been recreated as rules in {kib} {alert-features}. For this reason, the existing -{watcher} email action +NOTE: The default {watcher} based "cluster alerts" for {stack-monitor-app} have +been recreated as rules in {kib} {alert-features}. For this reason, the existing +{watcher} email action `monitoring.cluster_alerts.email_notifications.email_address` no longer works. -The default action for all {stack-monitor-app} rules is to write to {kib} logs +The default action for all {stack-monitor-app} rules is to write to {kib} logs and display a notification in the UI. To review and modify existing *{stack-monitor-app}* rules, click *Enter setup mode* on the *Cluster overview* page. @@ -47,21 +47,21 @@ checks on a schedule time of 1 minute with a re-notify interval of 1 day. This rule checks for {es} nodes that use a high amount of JVM memory. By default, the condition is set at 85% or more averaged over the last 5 minutes. -The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. +The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-missing-monitoring-data]] == Missing monitoring data -This rule checks for {es} nodes that stop sending monitoring data. By default, +This rule checks for {es} nodes that stop sending monitoring data. By default, the condition is set to missing for 15 minutes looking back 1 day. The default rule checks on a schedule -time of 1 minute with a re-notify interval of 6 hours. +time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-thread-pool-rejections]] == Thread pool rejections (search/write) -This rule checks for {es} nodes that experience thread pool rejections. By +This rule checks for {es} nodes that experience thread pool rejections. By default, the condition is set at 300 or more over the last 5 minutes. The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. Thresholds can be set independently for `search` and `write` type rejections. @@ -72,14 +72,14 @@ independently for `search` and `write` type rejections. This rule checks for read exceptions on any of the replicated {es} clusters. The condition is met if 1 or more read exceptions are detected in the last hour. The -default rule checks on a schedule time of 1 minute with a re-notify interval of 6 hours. +default rule checks on a schedule time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-large-shard-size]] == Large shard size This rule checks for a large average shard size (across associated primaries) on -any of the specified index patterns in an {es} cluster. The condition is met if +any of the specified data views in an {es} cluster. The condition is met if an index's average shard size is 55gb or higher in the last 5 minutes. The default rule matches the pattern of `-.*` by running checks on a schedule time of 1 minute with a re-notify interval of 12 hours. @@ -124,8 +124,8 @@ valid for 30 days. == Alerts and rules [discrete] === Create default rules -This option can be used to create default rules in this kibana space. This is -useful for scenarios when you didn't choose to create these default rules initially +This option can be used to create default rules in this Kibana space. This is +useful for scenarios when you didn't choose to create these default rules initially or anytime later if the rules were accidentally deleted. NOTE: Some action types are subscription features, while others are free. diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6345f310d25da..41bf27c7706a9 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -158,7 +158,7 @@ export class DocLinksService { introduction: `${KIBANA_DOCS}index-patterns.html`, fieldFormattersNumber: `${KIBANA_DOCS}numeral.html`, fieldFormattersString: `${KIBANA_DOCS}field-formatters-string.html`, - runtimeFields: `${KIBANA_DOCS}managing-index-patterns.html#runtime-fields`, + runtimeFields: `${KIBANA_DOCS}managing-data-views.html#runtime-fields`, }, addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, kibana: `${KIBANA_DOCS}index.html`,